Visual Basic LINQ講座 |
最も小さなLINQからはじめて、基本的な構文であるFrom句、Where句、Select句を説明します。 最後にはLINQの練習問題集もあります。
概要 ・最も小さいLINQ
・Inに指定できるのはFor EachのInに指定できるものと同じ。
・データソースから条件をつけて結果セットを抽出するにはWhere句を使用する。
・結果セットに含む項目を指定するにはSelect句を使用する。
・Select句にはカンマで区切って複数の項目を指定することができる。 |
前回も説明したようにLINQとは元となるグループ(=データソース)を操作して結果として別のグループ(=結果セット)を得る機能のことです。
LINQを使用するには専用の構文を使いますが、その構文で表現するのは要するにデータセットからどのような条件で、値を抽出・集計などの操作をして結果セットを導き出すのかということになります。
LINQの構文にはクエリ式の構文とメソッドベースの構文の2種類があり、クエリ式の構文の方が主流です。
LINQ講座第2回ではこのクエリ式の構文の基本的な部分の説明を通して、LINQの基本的なふるまいを説明していきます。
それから、「クエリ」という言葉をよく使うので馴染みのない人のために説明しておきます。「クエリ」とは英語の「query」のことで直訳すると「照会」です。これはデータベースなどのデータのあつまりに対して行う検索やデータの要求の内容および動作を指しています。
たとえば、電話番号で104に回すと目的の相手の電話番号を教えてくれるサービスがありますが、これは電話番号に対するクエリです。このとき「マイクロソフトの電話番号を教えてください。」と依頼したとするとこれがクエリの内容です。
まず、もっとも小さいLINQを紹介しましょう。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト1
このプログラムはデータソースに対して何も処理をしないでそのまま結果セットを生成します。
ですから、実行するとListBox1には「アメンボ」、「イノシシ」、「ウマ」、「エリマキトカゲ」、「オオカミ」と表示されます。
なんの処理もしないもっともシンプルなLINQというわけです。
LINQの部分だけ抜き出してみます。
From animal In animals |
このFrom 〜 In 〜(読み方:From = フロム、In = イン)というのはLINQでもっともよく使われるパーツです。Fromの後ろにはデータソースの要素を表現するための変数を指定し、Inの後ろにはデータソースを指定します。
このFrom 〜 In 〜の部分のことを「From句」と呼びます。Inの部分も含めてFrom句と呼びますのでヘルプなどを参照する際には言葉使いに注意してください。
「データソースの要素を表現するための変数」のことを「反復変数」または「範囲変数」と呼びます。上記の例ではanimalが反復変数です。反復変数はDimなどで事前に宣言しておく必要はありませんがLINQの中でだけ使用できます。
From 反復変数 In データソース |
データソースというのは既に説明したように元となるグループのことです。データソースとして指定できるのはFor Each文のInに指定できるものと一緒です。もっと正確に言うとIEnumerable(Of T)インターフェースまたはIEnumerableインターフェースを実装しているクラスです。
ですから配列やList、Dirctionaryなどのコレクションをはじめとして、多くの集合をデータソースとすることができます。また、Form.ControlsやTextBox.Lines、Directory.GetFilesなどの集合を返すメソッドやプロパティの戻り値はたいてい配列かコレクションですからこういった機能とLINQを合わせて活用することができます。
ここで説明している「IEnumerableを実装している」という条件はLINQの中でもLINQ to Objectsについての話です。LINQ to ADO.NETやLINQ to XMLではこれとはまた別のものをデータソースとして扱います。詳細はもっと後の方の回でそれぞれのLINQを取り上げるときに説明します。どのLINQを扱う場合でもLINQの構文や動作は同じですので、まずは今回の解説記事を通して基本を身につけてください。 |
反復変数の型は上記の例では自動的にStringになります。この例ではanimalsがString型の配列なのでVB2008から導入された『型の推定』機能が働く からです。自分でStringを指定する必要はありません。
しかし、明示するためにあえてAs Stringを付けて次のように書くこともできます。
Dim results = From animal As String In animals |
■リスト2
この節の説明はちょっと小難しくなってしまいました。それほど重要なことを説明しているわけではないので読み飛ばして次の「4.Where句」にジャンプしていただいても構いません。 |
上記の例では、通常はAs Stringを付けても付けなくても機能・性能ともに同じでなので好みに応じて記述して構いませんが、データソースがIEnumerableインターフェースのみを実装し、IEnumerable(Of T)インターフェースを実装していない場合は『型の推定』ができないので自分でAs 型名を指定しないと後で説明する別のLINQの機能を十分に使用できなくなることがあります。 ただし、Asを付けることで型変換でビルドエラーになる可能性もあります。実際には変換できる型であってもコンパイラがそのことを認識できないでビルドエラーになる場合があるのです。
たとえば、HashTableに対する以下のLINQを考えてみます。反復変数にAs 型名をつけていないこの状態では有効なLINQです。
Dim
hash As New
Hashtable hash("A") = "Apple" hash("B") = "Banana" hash("C") = "Cat" hash("D") = "Dog" Dim query = From item In hash For Each item In query ListBox1.Items.Add(item.ToString) Next |
■リスト3
しかし、これでは意図したとおりにAppleやBananaは表示できません。Hashtableの個々の要素がDictionaryEntry型で、DictionaryEntry型から文字列型への変換が定義されていないからです。文字列型への変換が定義されていないものに対してToStringメソッドを使用しても、その型の名前を返すだけです。
Apple, Bananaなどを表示させるには、itemがDictionaryEntry型であることをどこかで明示したうえで、そのValueプロパティを表示するようにする必要があります。
このように、型を明示したいときには反復変数にAs …をつけることになります。
次の例では、Apple, Bananaなどが表示されます。
Dim hash As New
Hashtable |
■リスト4
この例の場合は、LINQの中で型を明示しなくても、For Each内で型を明示して処理することもできます。
ただそれどと反復変数itemがオブジェクト型になってしまい、いろいろな操作がやりにくくなります。すぐ後で説明するWhere句やSelect句の使用も型が特定されていないと効果的ではありません。
なお、リスト4のコードはOption Strict Onの場合ビルドエラーになります。私はOption Strict Offが好きなのですが、世間ではOn派の方々もいっぱいいらっしゃいます。Option Strict Onでこの現象を回避するにはOfTypeメソッド(読み方:OfType = オブタイプ)を使ってデータソース側の型を限定します。
Dim query = From item As DictionaryEntry In hash.OfType(Of DictionaryEntry)() |
■リスト5
これで実行できるようになります。
OfTypeメソッドはデータソースのうち指定した型の要素のみを処理するように指定するフィルタリングの機能です。ここでDictionaryEntry型を指定すればそもそもDictionaryEntry型しか処理しないようになるので型変換の問題はなくなり、VBもビルドエラーを生成しません。そして、実行時にはHashtableの内容は全部DictionaryEntry型なので処理も問題なく行われるというわけです。
OfTypeメソッドではなくCastメソッドを使用しても結果的に同じ動作を実現できます。Castメソッドはデータソースの全要素に対する型変換を実行します。そして、Hashtableの全要素はDictionaryEntry型なのでこの操作は常に成功します。 |
Asがある場合とない場合とで性能についてはよほどシビアなチューニングを求められているのでない限り大差ありません。私が少し試したところではAs付きとAsなしの場合で性能に明確な差があるという結果は出ませんでした。
実際のところはLINQを使う時にいちいちデータソースが実装しているインターフェースを調べるのも面倒せすから、まずはAsなしで書いてみて不都合があったらAsを付けるくらいのスタンスでいいかもしれません。気になる方は何にでもAsを付けるというスタンスもいいでしょう。
博士のワンポイントレッスン
|
さて、基本の基本である最小限のLINQはみなさんも書けるようになったことと思います。しかし、ここまでの説明は何しろ結果セットとデータソースの内容が同じという例でしたので、まったく実用性がまったくありませんでした。
次の説明するWhere句(読み方:Where = ホエア)を使用すると、データソースのうち条件に合致するものだけを結果セットに含むという抽出処理やフィルタリングと呼ばれる機能を実現できるようになります。
次の例では文字列型の要素を含むデータソースから、4文字のもののみを結果セットとして取得します。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Where animal.Length = 4 '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト6
見ればわかると思うのですが、この例のように抽出条件を指定するにはFrom句の後ろにWhere句をつけて、抽出したい条件をTrueまたFalseで指定します。「=」演算子がTrueまたはFalseを返す演算子であるということをお忘れなきように。つまり、Where句に指定できるものはIf文の条件指定部分に指定できるものと同じです。AndAlso、OrElse、And、Orなどの論理演算子を使用して複数の条件を指定することもできます。
From 反復変数 In データソース Where 条件 |
ですからIf文が書けるレベルであれば、LINQのWhere句による条件指定の方法自体は何も難しいことはない です。ここで注目したいのはWhere句で使用している変数animalです。ここで変数animalが使用できるのはFrom句で宣言している反復変数だからです。LINQの中ではこのようにデータソースの中に含まれる個々の要素を表現するときには共通して反復変数を使用します。
LINQの外側で宣言している変数や関数を使用することもできます。
たとえば、抽出条件をLINQの外側でワイルドカードとして生成しておいてLINQの中で使用する例を紹介します。
Dim filter
As
String =
"*マ*" 'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Where animal Like filter '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト7
この例では、「マ」がつく要素のみを結果要素に含めます。その判定の手段としてLINQの中でLike演算子とともに変数filterを使用していますが、このfilterがLINQの外で宣言されていることに注意してください。
LINQの中だろうが外だろうが適用範囲にあるものはすべて使用できるのです。これが言語に統合されているということの1つの意味です。 フォームに貼りついているTextBoxなどを直接LINQから参照することも当然可能です。
もちろんこの程度の条件であれば、次のようにすべてをLINQ内で済ませてしまうことも簡単にできます。
Dim results = From animal In animals Where animal Like "*マ*" |
■リスト8
LINQから外部の関数を呼び出す例も紹介しておきます。
次の例ではデータソースに含まれる動物名の中から哺乳類のものだけを返します。
Private
Sub Button1_Click(ByVal
sender As
System.Object, ByVal
e As
System.EventArgs) Handles
Button1.Click 'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Where IsMammal(animal) '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) End Sub |
'''
<summary>哺乳類かどうかを判定する</summary> |
■リスト9
結果セットに含む内容を明示的に指定するにはSelect句(読み方:Select = セレクト)を指定します。
From 反復変数 In データソース Select 項目 |
今までの例では、要素ごとに考えるとデータソースの要素と結果セットの要素は常に同じものでした。これがSelect句を使用することで何を結果セットの要素にするかを指定できるようになります。
次の例は文字列型の要素からなるデータソースから文字数の一覧を結果セットとして取得します。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Select CStr(animal.Length) '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト10
Select句の中で各要素の文字数を取得するために変数animalのLengthプロパティを使用しています。これはWhereのところでも説明したようにFrom句で反復変数としてanimalを定義しているためにできることです。そして、データソースanimalsが文字列型の配列であることから型の推定機能でanimalが文字列型となるので、文字列型、つまりStringクラスのプロパティLengthが使用できるようになるわけです。
Where句の場合と同様ここに外部の変数や関数を使用することもできます。
Select句の性質をよく理解していただくためにつまらない例を紹介します。この(LINQによる)クエリを実行すると結果セットはどうなると思いますか?
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Select "こんにちは" '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト11
実行すると結果セットは5つの「こんにちは」になります。ListBox1には
こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
、と5行にわたって「こんにちは」が表示されます。
これはSelect句で値"こんにちは"を直接指定しているため、要素がどのようなものであれ必ず"こんにちは"が返されるからです。そして、Where句でのフィルタリングなどもないので結果セットの要素の数はデータソースの要素の数と同じ5個になります。ですから、5個の「こんにちは」が返されるわけです。
Select句とWhere句を同時に使用することもできます。
From 反復変数 In データソース Where 条件 Select 項目 |
次の例では動物名が4文字であるものを抽出して平仮名にします。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Where animal.Length = 4 Select StrConv(animal, VbStrConv.Hiragana) '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト12
「4文字のもの」と言う条件はWhere句で表現されており、「結果セットの要素をデータソースの要素を平仮名にしたものにする」という点はSelect句で表現されています。
Where句とSelect句の順番は重要です。順番を入れ替えるとクエリの意味が変わるため特段の事情がない限りWhere句をSelect句の前に書くようにした方が無難です。
クエリの意味が変わるということには2つの理由があります。
1つは結果セットを取得するときにLINQは先頭方向から処理されていくということです。この例ではWhere句によって要素が3つ(アメンボ、イノシシ、オオカミ)に限定された後で、Select句によって平仮名への変換が実行されます。
もう1つはSelect句にはLINQ内の変数定義をリセットする効力があるということです。ですから、もしSelect句の後にWhere句を書いた場合、Where句ではFrom句で定義された反復変数にアクセスすることができません。ただし、Select句であらたな変数を定義することも可能で、それは後続のWhere句で参照することもできます。
From 反復変数 In データソース Select 変数名 = 項目 |
Select句で変数を定義するには「変数名 =」という構文を使用します。この構文は変数を定義するだけではなく、その変数を結果セットに含める効力もあります。
次の例ではSelect句でanimalNameという変数を定義します。結果セットは動物名になります。つまり、Where句での抽出などがないので結果セットはデータセットと一致して、アメンボ、イノシシ、ウマ、エリマキトカゲ、オオカミになります。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From animal In animals Select animalName = animal '結果セットを取得して表示 ListBox1.Items.AddRange(results.ToArray) |
■リスト13
この例では変数animalNameは定義はしているものの使用はしていないので意味がありません。
変数名が多少は役に立つ例を紹介するために、結果セットは動物名を ( ) でくくったものにしてみます。そして、4文字以上のものだけを結果セットとして採用するためにWhere句も使用します。
Dim results = From animal In animals Select animalName = "(" & animal & ")" Where animalName.Length >= 4 |
■リスト14
この例ではWhere句で変数animalNameを使用しています。animalNameはSelect句で定義されていることに注意してください。Select句の変数リセット効果のためにWhere句で反復変数animalを使用することはできません。
この例を実行すると「(ウマ)」が結果セットに含まれます。「ウマ」は2文字ですが、「(ウマ)」は4文字になるからです。animalNameは( ) でくくった動物名を格納しています。
この例でSelect句とWhere句の順番を逆にしたときのことを考えてみましょう。
Dim results = From animal In animals Where animal.Length >= 4 Select "(" & animal & ")" |
■リスト15
この例では結果セットには「(ウマ)」は含まれません。
博士のワンポイントレッスン
|
Select句ではカンマで区切って複数の値を返すこともできます。
次の例では動物名と、動物名の文字数の両方を結果セットに含めます。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From name In animals Select name, name.Length '結果セットを取得して表示 For Each animal In results ListBox1.Items.Add(animal.name) ListBox1.Items.Add(animal.Length) Next |
■リスト16
この例のクエリではSelect句にnameとname.Lengthの2つの項目を含んでいます。反復変数の名前がnameですから、 要するにデータソースの項目と項目の長さを返します。結果セットのイメージは次のようなものになります。
name | name.Length |
アメンボ | 4 |
イノシシ | 4 |
ウマ | 2 |
エリマキトカゲ | 7 |
オオカミ | 4 |
それでは結果セットは何型のコレクションになるでしょうか?もはや文字列型とか数値型とか単純な型では表現できません。
VBはこのようなクエリを実行すると自動的に新しい型を生成します。これはVB2008から導入された驚くべき新機能の1つです。動的に新しい型を生成してしまうこの機能のことを単純に「匿名型」(読み方:匿名型 = とくめいがた)と呼びます。
この場合はnameプロパティとname.Lengthプロパティを持った型があれば結果セットを表現できるということはクエリのSelect句を見れば容易に導くことができます。ですから、VBはこの2つのプロパティを持つ型を生成してしまします。この新しい型には名前がありません。そこで「匿名」という表現を使って「匿名型」と呼ぶわけです。
ただし、プロパティの名前にドットが使用できないので実際にはnameプロパティとLengthプロパティを持った型が生成されます。
匿名型とはこのように自動的に新しい型を生成する機能およびこのようにして生成された型を指します。
プログラム画面でマウスポインタをresultsに合わせると型のところに「IEnumerable(Of <匿名型>)」と表示されます。匿名型についてはLINQ講座ではこれ以上は深入りしないでおきます。興味がある方はMSDNライブラリなどで調べてみてください。
結果セットを表示するためのループではこのように生成された匿名型のnameプロパティとLengthプロパティにアクセスしています。プログラム中のインテリセンスにもちゃんとnameやLengthがメンバとして表示されます。しかも、それぞれの型まで認識しています。型の推定機能との見事な競演です。
匿名型で生成されるプロパティに名前を明示的に指定したい場合は、先ほど紹介したようにSelect句での変数定義を使用します。
次の例ではAnimalNameとNameLengthというプロパティをもった匿名型のコレクションを結果セットとして生成します。
'データソースの作成 Dim animals = New String() {"アメンボ", "イノシシ", "ウマ", "エリマキトカゲ", "オオカミ"} 'LINQで処理を定義 Dim results = From name In animals Select AnimalName = name, NameLength = name.Length '結果セットを取得して表示 For Each animal In results ListBox1.Items.Add(animal.AnimalName) ListBox1.Items.Add(animal.NameLength) Next |
■リスト17
今回はLINQを構成するクエリの基本From句とWhere句とSelect句を紹介しました。この3つだけでもいろいろとLINQを便利に使うことができるでしょう。
最後にLINQに慣れるための簡単な練習問題を提供しますので。いまいち理解が不足していると感じる方はチャレンジしてみてください。
次回はいったんクエリ式の構文からはなれて、LINQの実行のされ方やメソッドベースの構文を説明する予定です。
問題1→回答1、問題2→回答2、問題3→回答3の順番に配置してあります。1つの問題にはA〜Bの小問があります。
回答は一例であってクエリ式の書き方はさまざまです。また、すべての問題はLINQの練習です。目的を達成するためにLINQを使わない方が効率的な場合もあります。
次の ( A )の部分にLINQを記述してプログラムを完成させなさい。
@システムフォルダにあるファイルでファイル名の最後が「.exe」のものの一覧を表示する。
Dim
folder = Environment.GetFolderPath(Environment.SpecialFolder.System) Dim fileNames = IO.Directory.GetFiles(folder, "*") Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
ヒント:ファイル名の最後が「.exe」か判断するには、String.EndsWithプロパティを使ってfileName.EndsWith(".exe")のように記述します。
A「ア」から始まらない国名の一覧を表示する。
Dim
names = New String()
{"アメリカ", "カナダ",
"アルバニア", "アルゼンチン",
"コスタリカ"} Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
BStopWatchクラスのメソッドのうち、引数がないものの名前の一覧を表示する。
Dim
methods = GetType(Stopwatch).GetMethods Dim query = ( A ) For Each method In query ListBox1.Items.Add(method.Name) Next |
ヒント:メソッドの引数の数はMethodInfoクラスのGetParameters.Countで取得可能。 結果セットにはプロパティアクセス用の隠しメソッドget_…やset_…も含まれてしまいますがよいものとします。
@
From filename In fileNames Where filename.EndsWith(".exe") |
A
From name In names Where name(0) <> "ア" |
B
From method In methods Where method.GetParameters.Count = 0 |
次の ( A )の部分にLINQを記述してプログラムを完成させなさい。
@システムフォルダにあるファイルでフォルダ名を含まないファイル名のみの一覧を表示する。
Dim
folder = Environment.GetFolderPath(Environment.SpecialFolder.System) Dim fileNames = IO.Directory.GetFiles(folder, "*") Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
ヒント:フルパスからファイル名を取得するにはIO.Path.GetFileNameメソッドを使用します。
A動物名の一覧で平仮名の部分をカタカナにした一覧を表示する。
Dim
names = New String()
{"カラス", "きりん",
"くま", "けながネズミ",
"コウモリ"} Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
ヒント:平仮名をカタカナにするにはStrConv関数を使用して、第2引数にはVbStrConv.Katakanaを指定します。
Bフォームに直接貼り付いているコントロールの名前の一覧を表示する。
'フォームに直接貼りついているコントロールの一覧を取得 Dim targets = Me.Controls.OfType(Of Control)() Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
@
From filename In fileNames Select IO.Path.GetFileName(filename) |
A
From name In names Select StrConv(name, VbStrConv.Katakana) |
B
From control In targets Select control.Name |
次の ( A )の部分にLINQを記述してプログラムを完成させなさい。
@システムフォルダにあるファイルについて、フルパスを含まないファイル名とファイルサイズの一覧を取得する。
Dim
folder As New
IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.System)) Dim size1MB As Long = 1024 * 1024 Dim query = ( A ) For Each file In query ListBox1.Items.Add(file.Name) ListBox1.Items.Add(" " & file.Extension) Next |
ヒント:DirectoryInfoからファイルの一覧を取得するにはGetFilesメソッドを使用する。
A現在のプログラムの中でDimからはじまる行で、かつDimと同時に値のセットも行っている行の内容の一覧を表示する。※ただし、すべての行にコメントがないことを前提とする。
Dim
fileName = New StackFrame(True).GetFileName Dim lines = IO.File.ReadAllLines(fileName) Dim query = ( A ) ListBox1.Items.AddRange(query.ToArray) |
注意:この例はデバッグモードでしか実行できません。
ヒント:変数linesは文字列型の配列で、プログラムの各行を格納しています。DimからはじまるかはStringクラスのStartsWithメソッドで簡単に調べられますが、プログラムの各行にはインデントがついているのでまずは前のスペースをTrimで除去する必要があります。Dimと同時に値のセットを行っているかはDimの行に「=」を含んでいるか調べるとわかります。文字列に「=」が含まれているか調べるにはStringクラスのContainsメソッドでわかります。
@
From file
In folder.GetFiles _ Where file.Length > size1MB _ Select file.Name, file.Extension |
A
From line
In lines _ Select trimed = line.Trim _ Where trimed.StartsWith("Dim ") AndAlso trimed.Contains("=") |
または、
From line
In lines _ Where line.Trim.StartsWith("Dim ") AndAlso line.Contains("=") _ Select line.Trim |