Visual Basic 9.0 概要
Erik Meijer, Amanda Silver, and Paul Vick Microsoft
Corporation
September 2005
日本語版最終更新日 2005 年 11 月 22 日
要約: Visual Basic 言語機能の概要および、データ集約的プログラミングをサポートする、新しい言語拡張機能について説明します。
目次
はじめに Visual Basic 9.0 とともにスタート 暗黙の型のローカル変数 オブジェクトとコレクションの初期化 匿名型 より深い XML サポート クエリの理解 拡張メソッド ネスト関数 Nullable 型 緩いデリゲート 動的インターフェース (あるいは、 強い "ダック タイピング") 動的識別子 まとめ
はじめに
Visual Basic 9.0 (Visual Basic コードネーム Orcas) は、統一された方法でデータ集約的なプログラム (リレーショナル データベース、 XML ドキュメント、およびオブジェクト グラフの、作成、更新、およびクエリ) をサポートするために、Visual Basic 8.0 (Visual Basic コードネーム Whidbey) 上にいくつかの拡張言語を構築しました。加えて、Visual Basic 9.0 では、場合によって利用可能な静的型付け、そして必要な動的型付けというVisual Basic 固有の機能を高めた、新しい言語機能を搭載しています。新しい機能を以下に示します。
- 暗黙の型のローカル変数
- クエリの理解
- オブジェクトの初期化
- 匿名型
- Linq フレームワークとの完全統合
- より深い XML サポート
- 緩いデリゲート
- Nullable 型
- 動的インターフェース
- 動的識別子
この文書は、これらの新しい機能の非公式な概要です。Visual Basic 言語定義の更新やコンパイラのプレビューは、Visual Basic デベロッパー センター http://www.microsoft.com/japan/msdn/vbasic/) をご覧下さい。
Visual Basic 9.0 とともにスタート
これらの新しい言語機能の現場での実力を見るために、まずは実際の例から始めましょうThe CIA World
Factbook database (英語)。データベースには世界の国々の様々な地理、経済、社会、政治に関する情報が含まれています。ここでの例として、それぞれの国の名前、首都、総面積、人口のスキーマから始めます。このスキーマを Visual Basic 9.0 で表現するために、以下のクラスを使用します。
Class Country
Public Property Name As String
Public Property Area As Float
Public Property Population As Integer
End Class
以下が、今回の例で使用する、国データベースの小さなサブセットです。
Dim Countries = _
{ new Country{ _
.Name = "Palau", .Area = 458, .Population = 16952 }, _
new Country{ _
.Name = "Monaco", .Area = 1.9, .Population = 31719 }, _
new Country{ _
.Name = "Belize", .Area = 22960, .Population = 219296 }, _
new Country{ _
.Name = "Madagascar", .Area = 587040, .Population = 13670507 } _
}
このリストを踏まえた上で、以下のクエリを使用して人口が 100 万未満の国をクエリすることができます。
Dim SmallCountries = Select Country _
From Country In Countries _
Where Country.Population < 1000000
For Each Country As Country In SmallCountries
Console.WriteLine(Country.Name)
Next
マダカスカルだけが 100 万以上の居住者を抱えているので、クエリがコンパイルされ実行された際には、以下の国名リストが上記のプログラムにより出力されます。
Palau
Monaco
Belize
記述をこれだけ簡単にした Visual Basic 9.0 の機能を理解するため、プログラムを解析してみましょう。まずは、Countries 変数の宣言です。
Dim Countries = _
{ new County { .Name = "Palau", .Area = 458, .Population = 16952 }, _
... _
}
新しい object-initializer を使用し、new
Country {..., .Area = 458, ...} などの複雑なオブジェクト インスタンスを、既存の With ステートメントに似た、簡潔な表現の構文で作成します。
この宣言は、コンパイラが宣言の右側にある初期化表現から、ローカル変数 Countries の型を推測する、implicitly typed local-variable 宣言も記述しています。上記の宣言は、明示的な型のローカル変数宣言である Country() 型に、正確に相当するものです。
Dim Countries As Country() = {...}
繰り返しますが、これはまだ強く型付けされた宣言です。コンパイラは自動的に、ローカル宣言の右側の型を推測し、プログラマは手動でデータ型をプログラムに打ち込む必要がありません。
ローカル変数宣言である SmallCountries は、SQL スタイルである query comprehension により初期化され、100 万以下の住民を抱えるすべての国をフィルタします。SQL との類似性は意図されたものであり、SQL を既に知っているプログラマにとっては、Visual Basic クエリ構文を覚えるのは容易です。
Dim SmallCountries = Select Country _
From Country In Countries _
Where Country.Population < 1000000
また、暗黙の型のアプリケーションがもう 1 つある、という点にも注意して下さい。コンパイラは、SmallCountries を IEnumerable(Of Country) であると推測します。コンパイラは、そのクエリ自体を、標準のクエリ操作に翻訳します。この場合、翻訳とは、以下に示してあるように簡単なものです。
Function F(Country As Country) As Boolean
Return Country.Population < 1000000
End Function
Dim SmallCountries As IEnumerable(Of Country) = _
Countries.Where(AddressOf F)
展開された文法は、デリゲートの AddressOf F として、コンパイラ生成のローカル関数を、IEnumerable(Of T) インターフェイスの拡張として、標準クエリ操作ライブラリに定義されている、拡張関数である Where に渡しています。
ここまで Visual Basic 9 の新しい機能のいくつかを見てきましたので、ここからはさらに詳細な概要を掘り下げて見ていきましょう。
暗黙の型のローカル変数
暗黙の型のローカル変数宣言では、ローカル変数の型は、ローカル宣言ステートメントの右側にある、初期化表現から推測されます。たとえば、コンパイラは以下の全ての変数宣言から、型を推測します。
Dim Population = 31719
Dim Name = "Belize"
Dim Area = 1.9
Dim Country = New Country{ .Name = "Palau", ...}
したがって、これらは以下の明示的な型の宣言と、正確に同等なものとなります。
Dim Population As Integer = 31719
Dim Name As String = "Belize"
Dim Area As Float = 1.9
Dim Country As Country = New Country{ .Name = "Palau", ...}
ローカル変数宣言の型はデフォルトで推測されるので、Option Strict の設定が何であろうと、このような変数へのアクセスは、早い段階で行われるものです。Visual Basic 9.0 ではプログラマは、以下のように Object 型として、変数を明示的に宣言することにより、遅延バインディングを明示的に指定しなければなりません。
Dim Country As Object = new Country{ .Name = "Palau", ... }
明示的遅延バインディングの要求は、誤って遅延バインディングを使用することを防ぎ、さらに重要なことに、下に示すような XML などの新しいデータ型に対する、力強い遅延バインディングの拡張を提供します。既存の動きを切り替える任意のプロジェクト レベルでのスイッチもあります。
For...Next や For Each...Next ステートメントなどのループ コントロール変数も、暗黙な型の変数になり得ます。For Dim I = 0 To Count や For Each Dim C In SmallCountries などでループ コントロール変数が指定された際、識別子は初期化表現やコレクション表現から推測され、ループ全体にスコープされる、新しく暗黙な型のローカル変数を宣言します。この For の右側での Dim の使用は、Visual Basic 9.0 での新しいものであり、暗黙な型のループ コントロール変数です。
この型推測のアプリケーションにより、以下のように、全ての小さい国を出力するループを書き直すことができます
For Each Dim Country In SmallCountries
Console.WriteLine(Country.Name)
Next
Country の型は、SmallCountries の要素型である Country であると推測されます。
オブジェクトとコレクションの初期化
Visual Basic では、With ステートメントにより、対象の式を複数回指定せずに、集合値の複数のメンバへのアクセスを簡素化します。With ステートメント ブロックの中では、ピリオドで始まるメンバ アクセス式は、対象となる With ステートメントの式がピリオドよりも優先されるように扱われます。たとえば、次のステートメントは新しい Country インスタンスを初期化し、その後、必要な値に自身のフィールドを初期化します。
Dim Palau = New Country()
With Palau
.Name = "Palau"
.Area = 458
.Population = 16952
End With
Visual Basic 9.0 の新しい object initializers は、複雑なオブジェクト インスタンスを簡単に作成するための、With の表現ベースのフォームです。object initializers を使用し、上記の 2 つのステートメントを、次のように 1 つの (暗黙の型の) ローカル宣言にすることができます
Dim Palau = New Country { _
.Name = "Palau", _
.Area = 458, _
.Population = 16952
}
このようなやり方での、式からのオブジェクト初期化は、クエリにとって重要です。一般にクエリは、等号符の右側にある Select 句により初期化された、オブジェクト宣言のように見えます。Select 句により式が返されるので、1 つの式でオブジェクト全体を初期化できなければなりません。
ここまで見てきたように、object initializers は、複雑なオブジェクトのコレクションを作成するのにも便利です。Add メソッドをサポートするコレクションは、どれも collection initializer を使用して初期化することができます。たとえば、部分的クラス (Partial Class) として都市 (cities) を宣言します。
Partial Class City
Public Property Name As String
Public Property Country As String
Public Property Longitude As Float
Public Property Latitude As Float
End Class
次にあるように、例に使用した国々の首都の List(Of City) を作成することができます。
Dim Capitals = New List(Of City){ _
{ .Name = "Antanarivo", _
.Country = "Madagascar", _
.Longitude = 47.4, _
.Lattitude = -18.6 }, _
{ .Name = "Belmopan", _
.Country = "Belize", _
.Longitude = -88.5, _
.Latitude = 17.1 }, _
{ .Name = "Monaco", _
.Country = "Monaco", _
.Longtitude = 7.2, _
.Latitude = 43.7 }, _
{ .Country = "Palau",
.Name = "Koror", _
.Longitude = 135, _
.Latitude = 8 } _
}
この例でも、ネスト化された object initializers を使用しており、ネスト化されたイニシャライザのコンストラクタはコンテキストから推測されています。この場合、それぞれのネスト化されたイニシャライザは、フル フォームである New City{...} と正確に一致します。
匿名型
しばしば、クエリの結果として、ある型のメンバを外したり project アウトしたい場合があります。たとえば、ソース データの Latitude (緯度) 列と Longitude (経度) 列を使用して回帰線を特定し、熱帯地方のすべての首都の Name (名前) と Country (国) だけが知りたいが、結果にはこれらの列を含めたくないということがあるかも知れません。Visual Basic 9.0 では、緯度が北回帰線と南回帰線の間にある各都市に型の名前をつけるのではなく、新しいオブジェクト インスタンス C を作成し、それを実現することができます。
Const TropicOfCancer = 23.5
Const TropicOfCapricorn = -23.5
Dim Tropical = Select New{ .Name = City.Name, .Country = City.Country } _
From City In Capitals _
Where TropicOfCancer =< City.Latitude _
AndAlso City.Latitude >= TropicOfCapricorn
推測された型であるローカル変数 Tropical は、匿名型のインスタンスのコレクションであり、IEnumerable(Of { Name As String, Country As String }) と呼ばれます。Visual Basic のコンパイラは、新しいシステム生成のクラスを作成します。たとえば、以下のような、メンバの名前と型が object initializers から推測された、 _Name_As_String_Country_As_String_ です。
Class _Name_As_String_Country_As_String_
Public Property Name As String
Public Property Country As String
Public Default Property Item(Index As Integer) As Object
...
End Class
同じプログラム内では、コンパイラは同様の匿名型を統合します。同じ順番で、同じ名前と型のプロパティのシーケンスを指定する、2 つの匿名 object initializers は、同じ名前の匿名型のインスタンスを作成します。外部的には、Visual Basic 生成の匿名型は、Object に消されてしまします。これは、コンパイラに匿名型を関数の引き数と結果として一様に渡すことを許可します。Visual Basic コードの使用に限って言えば、コンパイラは _Name_As_String_Country_As_String_ という型は、実際には匿名型の { Name As String, Country As String} を表していることを覚えておくために、特別にカスタマイズされた属性で生成されたクラスを装飾します。
匿名型は一般的に既存の型のメンバを予測するために使用されるので、Visual Basic 9.0 は短縮された予測記号 New { City.Name, City.Country } を、New { .Name = City.Name, .Country = City.Country} の短縮として許可しています。クエリの結果表現として使用される際は、予測イニシャライザを以下のようにさらに短縮することもできます。
Dim Tropical = Select City.Name, City.Country _
From City In Capitals _
Where TropicOfCancer =< City.Latitude _
AndAlso City.Latitude >= TropicOfCapricorn
これらの省略フォームは、省略されていないフォームと同一の意味を持つ、という点に注意して下さい。
より深い XML サポート
XLinq は、Language-Integrated Query Framework などの最新の .NET フレームワークの機能を最大限利用する為に設計された、インメモリ XML プログラミング API です。既存の標準 .NET Framework クエリ演算子を使用した便利な文法によってクエリが使いやすくなるのと同様に、Visual Basic 9.0 では XML リテラルと XML の遅延バインディングによって、XLinq のサポートが強化されています。
XML リテラルを説明するために、もともと階層のないリレーショナル データソース Countries と Capitals にクエリを投入し、それぞれの国の首都を子要素としてネストし、人口密度を属性として計算する、階層的 XML モデルを構築します。
該当する国の首都を見つけるために、それぞれの国の名前メンバとそれぞれの都市の国メンバを join します。これらの国とその首都から、埋め込み表現 holes に計算した値を設定することで、XML フラグメントを容易に構築することができます。"hole" の名前属性を括弧で囲み Name=(Country.Name) のように書き、"hole" の子要素を ASP.NET の文法で使用される山型括弧で囲み <Name><%= City.Name %></Name> のように書きます。以下が、XML と結合されたクエリです。 Dim CountriesWithCapital As XElement = _
<Countries>
<%= Select <Country Name=(Country.Name)
Density=(Country.Population/Country.Area)>
<Capital>
<Name><%= City.Name %></Name>
<Longitude><%= City.Longitude %></Longtitude>
<Latitude><%= City.Latitude %></Latitude>
</Capital>
</Country> _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country %>
</Countries>
XElement 型は宣言から省略可能で、その場合は他のローカル宣言と同じように推測されます。明示的な型は、以下のポイントを説明するために、この例では残しておきます。
この宣言では、Select クエリの結果を <Countries> 要素内部で置換しています。したがって、Select クエリが最初の "hole" の内容となり、<Countries> の内側で ASP.NET のスタイル タグとしてよく使用されている <%= と %> で区切られています。Select の結果も XML リテラルも共に表現であるため、Select 自身に他の XML リテラルを当然のようにネストします。このネストされたリテラルの中に、Country.Name のネストされた属性 "holes" と計算された人口密度比率 Country.Population/Country.Area、および名前と首都と等位の要素のネストされた要素 "holes" が含まれます。
上記のクエリは、コンパイルされて実行されると、以下の XML ドキュメントを返します (スペース節約のため、フォーマットを変更しています)。
<Countries>
<Country Name="Palau" Density="0.037117903930131008">
<Capital>
<Name>Koror</Name><Longitude>135</Longitude><Latitude>8</Latitude>
</Capital>
</Country>
<Country Name="Monaco" Density="16694.21052631579">
<Capital>
<Name>Monaco</Name><Longitude>7.2</Longitude><Latitude>3.7</Latitude>
</Capital>
</Country>
<Country Name="Belize" Density="9.5512195121951216">
<Capital>
<Name>Belmopan</Name><Longitude>-88.5</Longitude><Latitude>17.1</Latitude>
</Capital>
</Country>
<Country Name="Madagascar" Density="23.287181452711909">
<Capital>
<Name>Antananarivo</Name>
<Longitude>47.4</Longitude><Latitude>-18.6</Latitude>
</Capital>
</Country>
</Countries>
Visual Basic 9.0 は、XML リテラルを標準の System.Xml.XLinq オブジェクトにコンパイルすることにより、 Visual Basic および XLinq を使用する他の言語間の完全な相互運用性を保障します。例にあるクエリでは、コンパイラによって生成されるコードは (もし表示できるとすれば) 次のようになります。 Dim CountriesWithCapital As XElement = _
New XElement("Countries", _
Select New XElement("Country", _
New XAttribute("Name", Country.Name), _
New XAttribute("Density", Country.Population/Country.Area), _
New XElement("Capital", _
New XElement("Name", City.Name), _
New XElement("Longitude", City.Longitude), _
New XElement("Latitude", City.Latitude)))
From Country In Countries, City In Capitals _
Where Country.Name = City.Country)
XML の構築に加えて、Visual Basic 9.0 では XML の遅延バインディングによる XML 構造へのアクセスも簡素化されています。つまり、Visual Basic コードの識別子は、XML 属性と XML 要素に実行時にバインドされる、ということです。たとえば、次の方法で例にある国々の人口密度を出力することができます。
-
CountriesWithCapital XML 構造から全ての "Country" 要素を取得するには、child axis CountriesWithCapital.Country を使用します。
-
Country 要素の "Density" 属性を取得するには、attribute axis Country.@Density を使用します。
- どれだけ階層が深くなろうとも、
Country 要素の子である全ての "Latitude" を取得するためには、文字通り 3 つの点がソースコードに書かれている descendants axis Country...Latitude を使用します。そして、
- 結果のシーケンスから最初の要素を選択するには、extension indexer の
IEnumerable(Of T) を使用します。
これらを全てまとめると、コードは次のようになります。
For Each Dim Country In CountriesWithCapital.Country
Console.WriteLine("Density = "+ Country.@Density)
Console.WriteLine("Latitude = "+ Country...Latitude(0))
Next
コンパイラは、対象となる宣言、アサイン、初期化の表現が、特定のタイプではなく、Object タイプである場合、標準オブジェクトに対する遅延バインディングを使用します。同様に、コンパイラは対象となる表現が XElement、XDocument、または XAttribute のタイプまたはコレクションである場合に、XML の遅延バインディングを使用します。
XML の遅延バインディングの結果として、コンパイラは次のように解釈します。
- 子軸表現
CountriesWithCapital.Country は、生の XLinq コールである CountriesWithCapital.Elements("Country") のように解釈され、Country 要素の "Country" という名前の子要素すべてのコレクションを返します。
- 属性軸表現
Country.@Density は、Country.Attribute("Density") のように解釈され、Country の "Density" という名前の単一子属性を返します。そして、
- 子孫軸表現である
Country...Latitude(0) は、ElementAt(Country.Descendants(Latitude),0) の組み合わせのように解釈され、Country 配下のあらゆる深さにある名前付きの要素すべてのコレクションを返します。
クエリの理解
クエリの解釈においては、SQL によく似ていながらも、一方では Visual Basic の見た目と使い心地によく合い、また他方では .NET 統合言語クエリ フレームワークにスムーズに統合されている、クエリの文法に統合した言語を提供します。
SQL の実装 (英語) に詳しい人は、既存の .NET Framework のシーケンス演算子にある、射影、選択、直積、グループ化、ソートなどのクエリ内で行われる処理を表す多くの合成関係代数演算子についてご存知でしょう。
クエリの解釈の意味は、クエリの解釈自体をシーケンス演算子に翻訳することにより定義されるので、既存の演算子は、スコープ内にあるシーケンス演算子のいずれかにバインドされます。これは、特定の実装をインポートすることにより、クエリの解釈の文法をユーザーが効果的に再びバインドすることができることを意味します。特に、クエリの解釈は DLinq 基盤を使用するシーケンス演算子の実装と、クエリの実行を複数のローカルまたはリモートのデータソースで行うローカル クエリ オプティマイザに再度バインドすることができます。既存のシーケンス演算子の再バインドは、基本的な COM プロバイダ モデルと似ており、同じインターフェースの実装方法が異なることにより、既存のアプリケーション コードを修正せずに多様な操作と配置の選択肢を提供することができます。
基本である、Select...From...Where... の解釈では、 Where 句での条件を満たす値をフィルタして出力します。一番最初に説明した例に、住民 が 100 万人未満の国を全て検索する方法がありました。
Dim SmallCountries = Select Country _
From Country In Countries _
Where Country.Population < 1000000
シーケンス演算子の内部では、It は現在の行にバインドされています。Me のように、It のメンバは自動的にスコープ内となります。It の概念は、XQuery のコンテキスト項目である "." と一致し、SQL の "*" のように使用することができます。たとえば、次のクエリを使用して、その首都とともに国の名前をコレクションとして返すことができます。 Dim CountriesWithCapital = _
Select It _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
このローカル宣言の推測された型は、
IEnumerable(Of { Country As Country, City As City }).
です。
Order By 句を使用して、クエリの結果をソート キーに従ってソートすることができます。ソート キーはいくつでも指定できます。たとえば、次のクエリでは全ての国の名前を面積で昇順に、人口で降順にソートして返すことができます。
Dim Sorted = Select Country.Name _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Order By City.Longtitude Asc, Country.Population Desc
Min、Max、Count、Avg、Sum などの集計演算子は、コレクションに対して処理を行い、1 つの値に集計します。次のクエリを使用して、人口の少ない国の数を数えることができます。
Dim N As Integer = _
Select Count(Country) _
From Country In Countries _
Where Country.Population < 1000000
SQL のように、集計に便利な文法が特別に用意されており、複数の集計操作を "組にする" 上で大変便利です。たとえば、人口の少ない国の数を数え、それらの国の平均人口密度を 1 つのステートメントで計算するには、次のように書きます。
Dim R As { Total As Integer, Density As Double } = _
Select New { .Total = Count(Country), _
.Density = Avg(Country.Population/Country.Area) } _
From Country In Countries _
Where Country.Population < 1000000
このような集計は、コンパイラが生成した集計関数を、集計なしの通常の結果セットに適用することを省略した表現です。
集計関数は、ソースとなるコレクションのパーティション化と組み合わせて最もよく使用されます。たとえば、熱帯にある国をすべてグループ化し、それぞれのグループのカウントを集計するとします。これを行うために、Group By 句を使用します。ヘルパ関数である IsTropical は、Country が熱帯性気候であるかどうかというテストをカプセル化しています。
Partial Class Country
Function IsTropical() As Boolean
Return TropicOfCancer =< Me.Latitude _
AndAlso Me.Latitude >= TropicOfCapricorn
End Function
End Class
このヘルパ関数で、上記と全く同じ集計を使用しますが、Country と Capital の入力コレクションの最初のパーティションは、Country.IsTropical が同じであるグループにグループ化されます。この場合、次のような 2 つのグループが作成されます。1 つには南国である Palau、Belize、Madagascar が含まれ、もう 1 つには熱帯の国ではない Monaco が含まれます。
| Key |
Country |
City |
Country.IsTropical()
= True |
Palau |
Koror |
Country.IsTropical()
= True |
Belize |
Belmopan |
Country.IsTropical()
= True |
Madagascar |
Antanarivo |
Country.IsTropical()
= False |
Monaco |
Monaco |
それから、合計の数と平均人口密度を計算し、これらの値を集計します。結果の型は、Total As Integer と Density As Double のペアのコレクションです。 Dim CountriesByClimate _
As IEnumerable(Of Total As Integer, Density As Double }) =
Select New { .Total = Count(Country), _
.Density = Avg(Country.Population/Country.Area) } _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Group By Country.IsTropical()
上記のクエリでは、Group By 句の結果が IEnumerable(Of Grouping(Of { Boolean, { Country As Country, City As City })) 型のグループ化された値のコレクションであり、上記のテーブルのように複雑であることが隠されています。このような、それぞれの Grouping アイテムには、キーを抽出する表現である Country.IsTropical() から派生した Key メンバと、キー抽出表現が同じ値を持つ国と都市の一意なコレクションを含む Group が含まれています。Visual Basic コンパイラは、ユーザー定義集計関数とこのようなグループ化を合成して、パーティションごとに集計を行うことで必要な計算を行います。
前の例では、クエリの最終結果を計算するには Country だけが必要ですが、それぞれの Group に、Country と Capital の両方が含まれていることに注意してください。Group By 句を使用することで、グループの事前選択が可能になります。たとえば、以下の表現を使用して、全ての国の名前を北半球と南半球に分けることができます。
Dim ByHemisphere As IEnumerable(Of Grouping(Of Boolean, String)) = _
Select It _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Group Country.Name By City.Latitude >= 0
これにより、{ New Grouping { .Key =
False, .Group = { "Madagascar", "Belize" }}, New Grouping { .Key =
True, .Group = { "Palau" }} というコレクションが返されます。
Visual Basic 9.0 のクエリの解釈は完全に
構造化 (英語) されており、クエリは静的な型の規則によってのみ、恣意的にネストされ、制限される、ということを意味します。構造化することによって、個々の部分的表現を独立して理解することで、全体のクエリを容易に理解できるようになります。また、その言語の意味と型の規則を簡単に、より明確に定義することもできます。設計原理としては、SQL の設計の基本となる原理とは異なります。SQL 言語は完全に構造化されたものではなく、コミュニティで蓄積されたデータベースの経験の上に発展する多くの特別な事例に対応する、アドホックな設計となっています。完全に構造化されていないため、一般に、個別の部分を理解しただけでは複雑な SQL を理解することはできません。
SQL が構造化されていない理由の 1 つに、基盤となるリレーショナル データ モデル自体が構造化されていない点があります。たとえば、テーブルにはサブ テーブルが含まれません。言い換えれば、全てのテーブルはフラットである必要があります。結果として、複雑な表現を小さな単位に分割せずに、SQL プログラマは、結果がフラットなテーブルで、SQL データ モデルに適合する、一体となる表現を書くことになります。Jim Gray の言葉を引用すれば、「コンピューター サイエンスにおいて、再帰的でないものは全て良くない」ということです。Visual Basic は CLR 型システムに基づいているため、他の型のコンポーネントとして現れる型には全く制限がありません。静的な型の規則だけではなく、他の表現のコンポーネントとして現れる表現の種類にも何の制限もありません。結果として、列やオブジェクト、XML だけではなく、アクティブ ディレクトリ、ファイル、レジストリのエントリなども、クエリ ソースとクエリ結果の一番の構成要素となります。
拡張メソッド
.NET Framework 標準クエリ基盤の根底にある能力の多くは、拡張メソッドからもたらされたものです。実際すべてのクエリは、コンパイラによって、スコープ内の名前空間により定義された標準クエリ演算子拡張メソッドに直接翻訳されます。拡張メソッドは、カスタム属性でマークされている共有メソッドであり、インスタンスとメソッドの文法によって呼び出されます。結果的に、拡張メソッドは既存の型と追加のメソッドを持つ構築された型に拡張されます。
拡張メソッドは主にライブラリの設計者を意識しているので、Visual Basic はそれらを宣言する言語文法を直接サポートしていません。代わりに、作者は必要なカスタム属性を、モジュールとメンバに直接添付し、拡張モジュールとしてマークします。次の例は、任意のコレクションに拡張メソッド Count を定義します。
<System.Runtime.CompilerServices.Extension> _
Module MyExtensions
<System.Runtime.CompilerServices.Extension> _
Function Count(Of T)([Me] As IEnumerable(Of T)) As Integer
For Each Dim It In [Me]
Count += 1
Next
End Function
End Module
角括弧文法は、Me を通常の変数の名前として使用することを許可するキーワードのエスケープであるということを思い出して下さい。この拡張メソッドは、インスタンス メソッドをシミュレートする共有メソッドであるため、Me という識別子を入力の名前として、実際のインスタンスメソッドの中で使用するように使うのは便利ですが、それがキーワードであるため共有メソッドの中で許可されていないことから、括弧で囲んでエスケープする必要があります。
拡張メソッドは単なる通常の共有メソッドであり、したがって操作するインスタンスのコレクションを明示的に提供することにより、他の Visual Basic 共有関数を呼び出すのと同じように、Count 関数を呼び出すことができます。
Dim TotalSmallCountries = _
MyExtensions.Count(Select Country _
From Country In Countries _
Where Country.Population < 1000000)
拡張メソッドは、通常の Imports ステートメントによって、スコープ内で使用することができます。これらの拡張メソッドは、最初のパラメータで指定された型の追加メソッドとして現れます。 Imports MyExtensions
Dim TotalSmallCountries = _
(Select Country _
From Country In Countries _
Where Country.Population < 1000000).Count()
拡張メソッドは、通常のインスタンス メソッドより優先順位が低くなります。通常の呼び出し表現処理により、適用されるインスタンス メソッドが見つからない場合は、コンパイラは呼び出しを拡張メソッドの呼び出しとして解釈します。
最も自然にこのクエリを書くには、前述の集計文法を使用することです。
Dim TotalSmallCountries = _
Select Count(Country) _
From Country In Countries _
Where Country.Population < 1000000
ネスト関数
Where、Select、SelectMany などの多くの標準クエリ オペレータは、関数として Func(Of S,T) の型をデリゲートする、拡張メソッドとして定義されています。コンパイラに基本のクエリ オペレータに翻訳させ、あるいは Visual Basic プログラマにクエリ オペレータを直接コールさせるためには、デリゲートを容易に作成できる必要性があります。特に、周囲のコンテキストを取り込むデリゲートである、closures を作成することができる必要があります。クロージャを作成させる Visual Basic の仕組みは、ネスト化されたローカル関数とサブルーチン宣言を通じてのものです。
ネスト化された関数の使用をお見せするために、System.Query 名前空間に定義された、行基盤クエリ オペレータを呼びます。拡張メソッドの 1 つに、テストが True であり残りのシーケンスをスキップする間に、シーケンスから要素を生成する、 TakeWhile 関数というものがあります。
<Extension> _
Shared Function TakeWhile(Of T) _
(source As IEnumerable(Of T), Predicate As Func(Of T, Boolean)) _
As IEnumerable(Of T)
OrderByDescending オペレータは、自身のアーギュメントコレクションを、実証されたソート キーで降順にソートします。
<Extension> _
Shared Function OrderByDescending (T, K As IComparable(Of K)) _
(Source As IEnumerable(Of T), KeySelector As Func(Of T, K)) _
As OrderedSequence(Of T)
全ての小さい国を見つけ出すもう 1 つの方法は、まず人口で国をソートし、それから 100 万人未満の住民を持つ全ての国を、TakeWhile を使用して抽出するという方法です。
Function Population(Country As Country) As Integer
Return Country.Population
End Function
Function LessThanAMillion(Country As Country) As Boolean
Return Country.Population < 1000000
End Function
Dim SmallCountries = _
Countries.OrderBy(AddresOf Population) _
.TakeWhile(AddresOf LessThanAMillion)
クエリ自体には必要とされるものではありませんが、Visual Baic はコンパイラによってローカル関数宣言に翻訳される、匿名関数とサブルーチン ("lambda expressions" と呼ばれる) のダイレクト文法をサポートします。
Nullable 型
リレーショナル データベースは、しばしば通常のプログラム言語とは一致しなく、プログラマ達が馴染みが薄い、Null になりえる値の意味を表します。データ集約型アプリケーションでは、これらの意味を明確に正しくハンドルすることは、非常に大事なことです。この必要性を理解し、"Whidbey" では CLR は、ジェネリック型の Nullable(Of T As Struct) を使用して、Nullable のランタイム サポートを追加しました。この型を使用して、 Integer、Boolean、Date などの値型の Nullable バージョンを宣言することができます。明確になるという理由で、Visual Basic 文法のNullable 型は T? です。
たとえば、全ての国が独立しているという訳ではないので、もし独立しているならその独立日を表す新しいメンバーを Country クラスに加えることができます。
Partial Class Country
Public Property Independence As Date?
End Class
配列型と同じように、以下の宣言のように、プロパティ名に Nullable 修飾語を加えることもできます。
Partial Class Country
Public Property Independence? As Date
End Class
Palau の独立日は #10/1/1994# ですが、 British Virgin Islands はイギリスの領土であり、したがってその独立日は Nothing です。
Dim Palau = _
New Country { _
.Name = "Palau", _
.Area = 458, _
.Population = 16952, _
.Independence = #10/1/1994# }
Dim VirginIslands = _
New Country { _
.Name = "Virgin Islands", _
.Area = 150, _
.Population= 13195, _
.Independence = Nothing }
Visual Basic 9.0 は 3 段階の値のロジックと、Nullable 値の Null 伝播演算をサポートし、これは算術、比較、論理的あるいはビットワイズ、シフト、ストリング、または型オペレーションが Nothing だった場合、結果は Nothing となることを意味します。もし双方のオペランドが正しい値であれば、オペレーションは基盤となるオペランドの値の上で実行され、結果は Nullable 型に転換されます。
Palau.Independence と VirginIslands.Independence は供に Date? という型を持っているため、コンパイラは以下の引き算のために Null 伝播演算を使用し、その結果、ローカル宣言のための推測された型 PLength と VILength は供に TimeSpan? となります。
Dim PLength = #8/24/2005# - Palau.Independence REM 3980.00:00:00
PLength の値は、どちらのオペランドも Nothing ではないため、3980.00:00:00 です。他方では、 VirginIslands.Independence の値が Nothing であるため、結果は再び TimeSpan? の型となりますが、Null 伝播のために VILength の値は Nothing となります。
Dim VILength = #8/24/2005# - VirginIslands.Independence REM Nothing
SQL では、比較オペレータは Null 伝播を行い、論理オペレータは 3 段階の値のロジックを使用します。If と While ステートメントでは、 Nothing は False と解釈されます。したがって以下のコードの断片では、 Else ブランチが使用されます。 If VILength < TimeSpan.FromDays(10000)
...
Else
...
End If
注意すべきは、3 段階の値のロジックでは、等式は X = Nothing をチェックし, Nothing = X は常に Nothing を評価します。X が Nothing であるかどうかをチェックするには、2 段階の値のロジック比較である、 X Is Nothing か Nothing Is X を使用するべきです。
ランタイムは、特に Object からボクシングとアンボクシングの時に、Nullable 値を扱います。ボクシングの時は、Nullable 値は Nothing (HasValue プロパティが False) を表し、その値は Null 参照にボックスされます。正しい値をボックスする時 (HasValue プロパティが True) 、基盤となる値はまずアンラップされ、ボックスされます。このため、ヒープのオブジェクトでダイナミック型である Nullable(Of T) を持つものはありません。このような全ての明白な型はどちらかと言うと単なる T です。2 元的に、 Object からの値を T, か Nullable(Of T) にアンボックスすることができます。しかしこの結果は、遅延バインディングが 2 段階か 3 段階の値のロジックのどちらを使用するか動的に決定することができなくなります。たとえば、2 つの数字を加算する事前バインディングを行い、1 つが Nothing であり、Null 伝播が使用され、結果が Nothing となります。
Dim A As Integer? = Nothing
Dim B As Integer? = 4711
Dim C As Integer? = A+B REM C = Nothing
しかし、同じ 2 つの値の追加に遅延バインディングを使用する際、遅延バインディングが、 A と B の動的型が Integer であり、 Integer? でないという事実に基づき、2 段階の値のロジックを使用するので、結果は 4711 になります。したがって、Nothing は 0 と解釈されます。
Dim X As Object = A
Dim Y As Object = B
Dim Z As Object = X+Y REM Z = 4711
正しい意味となるよう、Null 伝播オーバーロードを使用するよう、コンパイラに指示しなければなりません。 Operator +(x As Object?, y As Object?) As Object?
? オペレーターを使用して、どちらかのオペランドを Nullable 型に転換します。
Dim X As Object = A
Dim Y As Object = B
Dim Z As Object? = X?+Y REM Z = Nothing
注意すべきは、これはどんな型の T でも、T? を作成できなければならないという点です。基盤となる CLR Nullable(Of T As Struct) 型は non-nullable 型の構成のみのアーギュメントを含んでいます。Visual Basic コンパイラは T が non-nullable 型ではない、T? を T に消し、 T が non-nullable 型の値である時は、Nullable(Of T) に消します。コンパイラは、Visual Basic プログラム内では、双方が T? の静的型であることを覚えておくために、十分な内部メタデータをキープします。
緩いデリゲート
Visual Basic 8.0 で AddressOf や Handles を使用してデリゲートを作成する際、デリゲート識別子をバインドする目的のメソッドの 1 つは、デリゲートの型の署名と完全にマッチしなければなりません。以下の例では、 OnClick サブルーチンの署名は、Button 型の背後で宣言されている、イベント ハンドラ デリゲート Delegate Sub EventHandler(sender As Object, e As EventArgs) の署名と完全にマッチしなければなりません。
Dim WithEvents B As New Button()
Sub OnClick(sender As Object, e As EventArgs) Handles B.Click
MessageBox.Show("Hello World from" + B.Text)
End Sub
しかし、non-delegate 関数とサブルーチンを呼び出す際は、Visual Basic は呼び出そうとしているメソッドの 1 つと完全にマッチする、実際のアーギュメントを必要としません。以下の断片が示すように、正式なパラメータ Object と EventArgs のサブタイプである、Button 型と MouseEventArgs 型を使用して、OnClick サブルーチンを実際に呼び出すことができます。
Dim M As New MouseEventArgs(MouseButtons.Left, 2, 47, 11,0)
OnClick(B, M)
反対に、2 つの Object パラメータを使用するサブルーチン RelaxedOnClick を定義すると、実際のアーギュメントである Object と EventArgs 型を使用して、それを呼び出すことができます。
Sub RelaxedOnClick(sender As Object, e As Object) Handles B.Click
MessageBox.Show("Hello World from" + B.Text))
End Sub
Dim E As EventArgs = M
Dim S As Object = B
RelaxedOnClick(B,E)
Visual Basic 9.0 では、デリゲートへのバインディングはメソッド呼び出しと一貫性を保つために、緩められています。もし、正式なパラメータ、およびデリゲートの戻り型と完全に一致する実際のアーギュメントと、関数あるいはサブルーチンを呼び出すことが可能であれば、関数とサブルーチンをデリゲートにバインドすることができます。言い換えれば、デリゲートのバインディングと定義は、メソッド呼び出しが従う、同じオーバーロード解決ロジックに従います。
これは、Visual Basic 9.0 では、2 つの Object パラメータを Button の Click イベントに使用する、サブルーチン RelaxedOnClick にバインドできるということを意味します。
Sub RelaxedOnClick(sender As Object, e As Object) Handles B.Click
MessageBox.Show(("Hello World from" + B.Text)
End Sub
イベント ハンドラの 2 つのアーギュメント、sender と EventArgs は、殆ど関係ありません。代わりにハンドラは、イベントが直接登録され、自身の 2 つのアーギュメントを無視する、コントロールの状態にアクセスします。この一般的なケースをサポートするため、曖昧な結果ではない場合、デリゲートはアーギュメントを使用しないよう緩和することできます。言い換えれば、以下のように簡単に書くことができます。
Sub RelaxedOnClick Handles B.Click
MessageBox.Show("Hello World from" + B.Text)
End Sub
デリゲート緩和は AddressOf を使用してデリゲートを構築する時や、デリゲート作成表現時、メソッド グループが遅延バインド呼び出し時でさえ、適用されると理解されています。
Dim F As EventHandler = AddressOf RelaxedOnClick
Dim G As New EventHandler(AddressOf B.Click)
動的インターフェース (あるいは、強い "ダック タイピング")
C# や Java や Option Strict On の Visual Basic のように純粋な静的型の言語では、コンパイル時に存在する対象の式のメンバしか呼び出すことができません。たとえば、以下の 2 つ目のアサインメントは、Country クラスが Inflation メンバを持っていないため、当然コンパイルタイム エラーを起こします。
Dim Palau As Country = Countries(0)
Dim Inflation = Country.Inflation
しかし多くの場合で、静的に対象の型を知らなかったとしても、メンバにアクセスする必要があります。Option Strict Off により、Visual Basic は遅延バインディング メンバに Object 型の対象へアクセスを許可します。強力で非常に柔軟性はありますが、遅延バインディングにはコストが伴います。特に、 IntelliSense とタイプ インターフェイスを失い、事前バインディングの世界に戻るには、キャストや明示的な型が必要になります。
しばしば、遅延バインディング コール行っている時でさえ、値はある "インターフェイス" に密着している、と仮定しています。オブジェクトがそのインターフェイスを満たす限りは、問題はありません。動的言語コミュニティでは、これを "Duck Typing" と呼んでいます。それが Duck のように歩き、Duck のように話すのであれば、それは Duck です。Duck Typing の考えを説明するため、以下の例は Country、あるいは人表す新しい匿名型をランダムに返します。String 型の Name プロパティがどちらにもあります。
Function NamedThing() As Object
Dim Static RandomNumbers = New Random()
Dim I = RandomNumbers.Next(9)
If I < 5
NamedThing = Countries(I)
Else
NamedThing = New{ .Name = "John Doe", .Age = 42-I }
End If
End Function
Dim Name = CStr(NamedThing().Name)
遅延バインディングがCStr(NamedThing()) を呼び出す際、String 型の Name メンバを持っているNamedThing() によって返される値を静的に仮定します。 動的インターフェイス の新しい機能を使用して、この仮定を明示的にすることができます。静的型が動的インターフェイスであるターゲットは常に遅延バインディングを使用してアクセスされますが、メンバアクセスは静的型になります。これは、完全な IntelliSense とタイプ インターフェイスを得て、キャスティングや明示的型を行う必要がない、ということを意味します。
<Dynamic> _
Interface IHasName
Property Name As String
End Interface
Dim Thing As IHasName = NamedThing()
Dim Name = Thing.Name REM Inferred As String.
動的識別子
動的インターフェイスは、プログラマが、遅延バインディング呼び出しで予想されるメンバの名前と署名を、静的に知っているという前提に影響します。しかし、ある完全な動的なシナリオでは、静的に呼び出したいメンバの名前すら知らないかも知れません。動的識別子は、式を呼び出す識別子が、動的に計算されるところでは、極度な遅延バインディングを許可します。
次の例では、3 つのクラスを 3 つの異なる言語 (英語、オランダ語、およびフランス語) で定義され、それぞれの言語で "Name" フィールドを持ちます。
Class English
Name As String
End Class
Class Nederlands
Naam As String
End Class
Class Français
Nom As String
End Class
この Person の "Name" フィールドにアクセスするためには、型の名前を取得するためにその値を反映し、テーブルのメンバの名前を調べます。そして、Person の正しいメンバーを呼び出すために、動的識別子 アクセスを使用することができます。
Dim Dictionary = New Dictionary(Of String, String) { _
.Add("English", "Name"), _
.Add("Nederlands", "Naam"), _
.Add("Français", "Nom") }
Dim Person As Object = New Français { .Nom = "Eiffel" }
Dim Name As String = Person.(Dictionary(Person.GetType().Name))
まとめ
Visual Basic 9.0 ではさまざまな新しい機能が取り入れられています。この文書では、一連の関連する例を使用してこれらの機能を紹介しましたが、その根底にあるテーマも同様に強調される必要があります。
- リレーショナル データ、オブジェクト データ、および XML データ: Visual Basic 9.0 はリレーショナル データベース、 XML ドキュメント、任意のオブジェクト グラフ、またはメモリにあるソースへの独立したアクセスを統合します。この統合はスタイル、技術、ツール、プログラミング パターンにおいて行われています。特にフレキシブルな Visual Basic の文法により、XML リテラルや SQL ライクなクエリの解釈といった拡張を、言語に深く拡張することが簡単にできるようになります。これによって、新しい .NET Language Integrated Query API の数が大幅に減少し、IntelliSense と Smart Tag によるデータ アクセス機能の発見性を増加され、他言語の文法を文字列データからホスト言語に移動することによりデバッグが大幅に改善されます。将来的には、XSD スキーマを利用することにより XML データの整合性をさらにに増加させようとしています。
- 静的な型付けの利点によりダイナミックさを増す: 静的な型付けの利点はよく知られています。実行時ではなくコンパイル時のバグの特定、事前バインド アクセスによる高いパフォーマンス、ソース コードでの明示性による明瞭さ、などです。ですが、時には動的型付けの方が、コードがより短く、明確になり、フレキシブルになることもあります。もしある言語が、動的型付けを直接サポートしていない場合でも、プログラマが必要なときにリフレクション、辞書、ディスパッチ テーブル、および他の技術により少しずつ動的型付けを実装することができます。これにより、バグが紛れ込みやすくなり、メンテナンス費用が増加します。可能な限り静的型付けをサポートし、必要な場合には動的型付けをサポートすることにより、Visual Basic はプログラマーに両方のもっとも良い部分を提供します。
- プログラマへの認知的負荷を減らす: タイプ参照、オブジェクト初期化、緩いデリゲートなどの機能は、コードの冗長性およびプログラマが学習して記憶するか調査しなければならない多くの例外規則を減らしつつ、パフォーマンスには何ら影響を与えません。動的インターフェースのような機能により、遅延バインディングの場合でも IntelliSense がサポートされ、より進んだ機能の発見性が大幅に改善されます。
Visual Basic 9.0 の新機能のリストは長いように感じるかもしれません。ですが、上記のテーマによって、これが論理的かつタイムリーで、Visual Basic が世界で最も優れたプログラミング システムであると皆さんが納得されることを望んでいます。皆さんのイマジネーションが刺激され、私たちとともに、これがこの後やってくるさらにすばらしいものへの序章であるということにお気づきになることを望んでいます。
|