[C#] 型推論と暗黙のキャスト(implicit)を組み合わせる

Microsoft、「Metroスタイル」改称へ――米メディア報道 – ITmedia ニュース
http://www.itmedia.co.jp/news/articles/1208/03/news053.html

Metro Style という用語が消えるそうだけど「メトロスタイル」の可能性が低くなるのであれば、それはそれで歓迎かと。某メトロ本を書く前の文章練習として、少し書きつけます。

C# の var は、型推論という形で C# の型をコンパイラが判定するという機能で、宣言部のコードが短くなるということと、LINQ のクエリを受けるときに小難しい…というか記述不可能な書き方をしなくてよいのが大きな利点です。

CPoint pt = new CPoint();

のかわりに

var pt = new CPoint();

と書くわけです。これ、VB で書くと、

Dim pt As New CPoint()

なので、宣言部だけだとあまり意味がないのですが、

var query = from t in Table where t.name = “masuda” select t ;

のように LINQ のクエリを受ける場合は「ほぼ必須」ですよね。「ほぼ」っていうのは、IEmutable か IQuerable を使えばなんとか可能なのですが、インテリセンスの助けがないと確実に無理ッ!!! 素のテキストエディタを使う場合は「var」が必須になります。

C# は C++ の型の書き方を継承しているので、宣言をするときに、あたかも型を2つ書かないといけないという不便さが際立つのですが(VB だと1回だけでいいですよね)、もともと C++ の場合は、

CPoint pt ;

CPoint *pt = new CPoint();

といった異なる書き方をするので、必ずしも型が重複しているという訳ではありません。先のほうは内部変数でスコープを抜ける時にデストラクタが呼ばれる(自動的に解放される)ので、普通は上記の書き方を多用します。C# の場合は、using を使って

using ( CPoint pt = new CPoint() ) { … }

のようにスコープを決めることもできるのですが、なんかいまいちです。コーディングの経済性からいえばタイピングの少ない方に流れがちなので、私の場合は using はあまり使っていません。明示的に画像関係(Bitmapなど)を扱う時ぐらいです。

■暗黙のキャスト(implicit)

型推論と反対の発想(と私は思っている)ものに、暗黙のキャストがあります。型推論は、コンパイラが構文(関数の戻り値や別の型)から一意の型を決めますが、キャストはコードを書いている人=プログラマが型を決定します。余談ですが、F# の場合は型を推論すると同時に、’a という anonymous な型が用意されています。これはテンプレート/generic の T と同じように働くのですが、表面上は型がその場所で規定されているのか、呼び出し側で既定されているのかを区別しません。C++ や C# の場合は CPoint() のように「<>」を使って「不定の型」であることを示すところが大きな違いと感じています。ちなみに、VB の場合は「CPoint(Of Integer)()」となって実に奇妙な感じなのです。

LINQ や XML を扱っていると、ツリー構造なりテーブル構造が単数と複数の組み合わせであることがわかってきます。LINQ は、プログラムコードの中から for/while 文を排除していく故に複数行のコードがワンライナーで記述できる(数式に近くなる)という利点があるのですが、単数と複数の要素を暗黙のキャストを使って使い分けると、.FirstOrDefault や [0] とか != null を減らすことができます。

一例を示すと HtmlDom では、HTML タグに関して、HtmlElement という単数の要素と、HtmlElementCollection という複数の要素≒リストを用意しています。

class HtmlElementCollection : List<HtmlElement> { … }

ツリー構造なので、HtmlElement は、子要素として HtmlElementCollection Children というプロパティを持っています。

さて、とある要素に div という子要素があるかどうかを調べるのには、LINQ を使うと次のように書けるように作ります。

# @neuecc さんより
# LINQ to random thoughts – NyaRuRuの日記
# http://d.hatena.ne.jp/NyaRuRu/20080205/p1
# Any を使えという指摘が。そりゃそうだ。 Count すると全件検索してしまうがな orz

if ( root.Children.Any( n => n.TagName == “div” ) ) {

要素があるかどうかを Anyメソッドで調べるのですが、大抵の場合は「子要素があれば最初の要素がほしい」というのが普通です。そうなると、.First メソッドを使って、

var el = root.Children.First( n => n.TagName == “div” );

ってことになるのですが、子要素がない場合は、el の値は null になってしまいます。こうなると != null が必要になってちょっと面倒です。なので、.FirstOrDefault というメソッドを使って次のように書き換えます。

var el = root.Children.FirstOrDefault( n => n.TagName == “div” );

さて、ここで div は最初のひとつだけということにしてましたが、最後のひとつを取りたい時はどうするのでしょうか? うまい具合に Last メソッドや LastOrDefault メソッドがあるのでこれを活用します…ってことにヘルプ的にはなるのですが、こうやっていくと組み合わせ的にメソッド数が増えてしまいます。

ならば、

var el = root.Children.Where( n => n.TagName == “div” ).First() ;

のようにどうせ LINQ は遅延評価なのだから、Where で検索した後に First だけを持ってくるのが素直では?と思ってしまうわけです。まあ、実際にこういう書き方もできるわけで。そこで、暗黙のキャストを使って、First 自体も消してしまおうというのが、HtmlDom の発想(元ネタは ExDoc)です。

HtmlElement el = root.Children.Where( n => n.TagName == “div” ); // 最初の要素を取得

HtmlElementCollection els = root.Children.Where( n => n.TagName == “div” ); // 複数の要素を取得

のように、あえて「型」を記述させて「最初の要素(単数の要素)」なのか「リスト(複数の要素)」なのかをプログラマが決めます。Where メソッド自体はリストを返すのですが、HtmlElementCollection から HtmlElement への自動的なキャストを許すことで、こんな書き方ができます。

public class HtmlElementCollection :  List<HtmlElement>
{
    public static implicit operator HtmlElement(HtmlElementCollection col)
    {
        if (col.Count > 0)
        {
            return col[0];
        }
        else
        {
            return HtmlElement.EmptyElement;
        }
    }

遅延実行ではありませんが、上記の形で簡単に暗黙のキャストが実装できます。HtmlElementCollection から HtmlElement にキャストした時に、要素があれば先頭の要素を、要素がなければ EmptyElement という空の要素を返します。これは、.FirstOrDefault というメソッドと同じ動作をします。空であるかどうかは、null と比較するのではなく、IsEmpty というプロパティを使うわけです。

こうすることで、先の el という変数は null になりません。null にはならないので、el.IsEmpty や、el.Children.Where という書き方を続けてできます。Where メソッドを直列にする書き方は、LINQ でも同じで常に IEmutable というコレクションもどき(コレクションに見えるが、遅延実行されるので、内部的には実行メソッドの順序だけを保持しているという点で「もどき」)を使うことで実現されています。もう少し文法的に煮詰めたら HtmlDom もそうする予定です。

■要素を探すというコードが大半であるという現実

XML や HTML を扱う場合には「指定した id を持つ要素を探す」というコードが頻繁に発生します。ElementById というメソッドで、要素を探して値を取得したり設定したりします。for/while の場合は、2つの動作があって「要素に対して同じ操作を繰り返す」というのと「要素を探し出して、何か操作して終わる」というのが頻繁に発生します。

サンプルを書いて考えてみると、

□複数の要素を一気に更新

foreach ( var el in root.Children ) {
  if ( el.TagName == "div" ) {
    el.Value = "xxx";
  }
}

var q = root.Children.Where( n => n.TagName == "div" );
// LINQ の場合、連続して更新ってどうやるんだっけ?
foreach ( var el in q ) {
  el.Value = "xxx";
}

// HtmlDom の場合
root.Children.Where( n => n.TagName == "div" ).Value = “xxx”;

□ひとつの要素を見つけて更新

foreach ( var el in root.Children ) {
  if ( el.Attribute("id") == "m1" ) {
    el.Value = "xxx";
    break;
  }
}

// LINQ の場合
root.Children.Where( n => n.Attribute("id") == "m1" ).First().Value = "xxx";

// HtmlDom の場合
root.Children.Where( n => n.Attribute("id") == "m1" ).Value = "xxx";

上記のように HtmlDom では使って単数/複数を同じように扱うことができます…と云いますか、LINQ 構文内では、HtmlElementCollection として扱い、同じことが HtmlElement にも適用できる、という「見かけ上」の問題を解決しています。単数のほうは、正確に書くと HtmlElement にキャストすることになるので、

((HtmlElement)root.Children.Where( n => n.Attribute("id") == "m1" )).Value = "xxx";

ということになるのですが、動作としては「id=”m1”」である要素は、「全体の1つしかない」という「仕様」を前提にしてコーディングをするので、複数あるときはイリーガルな訳で無駄なのです。逆に、データの中にひとつもないというのもイリーガルなわけで、これらの処理は正確には「例外」として処理する必要があるのですが、コーディングとしては面倒だし「忘れ」そうですよね。そうそう、この「忘れる/漏れる」というヒューマンエラーを回避するためにも、単数/複数/null を一括して扱う implicit な訳です。安全側に倒れるという安全管理の手法です。データが多少イリーガルでもプログラムは落ちずになんとなく正常に動くというパターンです。

そんな訳で、LINQ の文法である from/where/select を活用して、同時に暗黙のキャストと組み合わせている HtmlDom ですが、公開はもうちょっと先かなぁと。遅延実行の実装がよくわからいのが本音で、もうちょっと LINQ の実装をながめなくては。

neuecc

カテゴリー: C# パーマリンク

[C#] 型推論と暗黙のキャスト(implicit)を組み合わせる への1件のコメント

  1. masuda のコメント:

    これ、改めて書いてみて気づいたのだけど、単数の HtmlElement を 複数の HtmlElementCollection に統一させたほうが?とか考え直す。
    その昔、ExDoc を作った時に、string s = HtmlElement.Value で、List lst = HtmlElementCollection.Value という発想だったので、単複は別という考え方だったのだが、どんなもんなのだろう。コーディング的に、どう書いてもなんとなく動くという主旨だから、これでいいのかな?
    var results= HtmlElement.Children.Where( … ) ;
    の結果は、この時点では単複を区別しない/確定しないという曖昧な状態で、その後で
    string v = results.Value ;
    あるいは
    List
    lst = results.Value ;
    と記述した時点で単複が決まるというシューレディンガー的な状態遷移が、という感じを目指しているのだから。

コメントは停止中です。