++C++; // 未確認飛行 C

関数型言語由来の新機能

目次

キーワード

概要

Ver. 3.0

C# 3.0 の新機能には、関数型言語や動的言語が由来と思われる機能がいくつかあります。 ただし、C# の方向性としては、「関数型・動的言語になる」ではなくて、 「関数型・動的言語との融合」です。

すなわち、C# が関数型・動的言語になったわけではなくて、 あくまで、関数型・動的言語の機能で、 手続き型・静的言語を基本とする C# でも実現できそうなものを輸入したという感じです。

具体的には、型の推論やラムダ式がそれにあたります。 これらの機能は、 クエリ式のために導入されたと思われる節が強いです。 それ以外の場面で使っても便利は便利なんですが、 メリットだけでなく多少の副作用もあったりするので利用の際には少し注意が必要です。

暗黙的型付け

var キーワードを用いて、 暗黙的に型付けされたローカル変数(Implicitly typed local variables)を定義できるようになりました。

var n = 1;
var x = 1.0;
var s = "test";

var を用いる際には、必ず初期値を伴う必要があります。 そして、初期値から、変数の型を自動判別してくれます。 上記の例では、 nintxdoublesstring 型の変数になります。

注意すべき点は、 あくまで型の自動判別・推定であって、 任意の型の値を代入できる万能な変数を作れるわけではないということです。 したがって、以下のように、初期値を伴わない宣言は(型の推定ができないので)エラーになります。

var n; // エラー。初期値が必要。

TypeName x = new TypeName(); というように、 式の両辺に型名を書かないといけないのは冗長ではあります。 var は、この冗長さを省くため、左辺側の型名を省略できる機能だと思ってください。

ちなみに、この機能は、後述する匿名型と併せて、 LINQ をより便利に使うためのものであって、 それ以外の場面ではあまり乱用すべきではないと思います。 型の自動判別・推定機構に頼らず、できる限りちゃんと型を明示すべきです。 むやみに型推定機能に頼ると、知らず知らずの間に int のつもりで使っていた変数が double になっていたということも起きかねません。

冗長性がエラー耐性になっている場合もあるので、 TypeName x = new TypeName(); という冗長な書き方も悪いことばかりではありません。

ラムダ式

ラムダ式(lambda expression)と言うのは、 関数型言語と呼ばれるような種類のプログラミング言語における用語なのですが、 関数(メソッド)を整数などの変数と全く同列に扱う手法のことです。

といっても、C# が関数型言語になったわけではありません。 関数型言語のように副作用を許さないわけではないですし、 末尾再帰に最適化がかかったりもしません。 とりあえず、匿名メソッドの発展形くらいに思っておいた方がいいかと思います。

C# 3.0 で導入されたラムダ式は、 以下のようなものだと思ってください。

  1. デリゲートに対して代入すると、 匿名メソッドと同じ扱いになる。
  2. Expression 型の変数に代入すると、式木(expression tree)データになる。
匿名メソッドの記法の簡略化

1つ目に関しては、 要するに、 C# 2.0 の匿名メソッドをさらに簡便な記法で書けるようにしただけのものです。 先に概要を書いてしまうと、以下のような感じ。

  • delegate とか { return } とかの記述を省略できる。
  • 型推論機構が働く。

匿名メソッドでできることはラムダ式でもできます。 C# 3.0 の開発者も、「もし、ラムダ式が先に導入されていれば、匿名メソッドの構文は不要だった」と言っています。

C# 2.0 までの匿名メソッドは、例えば、以下のような書き方をしていました。

delegate(int n)
{
  return n > 0;
}

この例の匿名メソッドをラムダ式を使って書き直すと、以下のようになります。

(int n) => n > 0;

要するに、記法としては、以下のようになります。

引数リスト => 

ちなみに、文脈的に引数の型が明らかな場合、 型は省略できます。 (var と似たような型推定機能が働く。) 例えば、以下のようなデリゲートがあるとき、

delegate bool Pred(int n);

このデリゲートに対する代入式中にラムダ式を書く場合、 引数の型は int であることが明らかなので、 int を省略して以下のように書くことができます。 (n の型はコンパイラが推論してくれます。)

Pred p = n => n > 0;

あと、いちいちデリゲートを定義するのは面倒なので、 .NET Framework 3.5 では、Func という名前のデリゲートが標準で用意されています。 Func はジェネリックスを使って定義されていて、 例えば、上述の例の Pred デリゲートのように、 int 型の引数を1つとって、bool 型を返すようなデリゲートを以下のように表現できます。

Func<int, bool>

また、式が複数になる場合は {} でくくります。 (この場合は、{} の中身は匿名デリゲートと同じ書き方をする。return も書く必要あり。)

Func<int, int, int> f =
  (x, y) =>
  {
    int sum = x + y;
    int prod = x * y;
    return sum * prod;
  };
式木(expression tree)

一方、2つ目に関してですが、こちらは完全に新機能で、ラムダ式特有のものです。 匿名メソッドと違って、 ラムダ式は本当に式木(expression tree)データとして扱うこともできます。

上述の例の Pred p = n => n > 0; のように、 デリゲートに代入する場合には、 ラムダ式は匿名メソッドと同じ扱い、 すなわち、コンパイル後には実行コードの状態になっています。

これに対して、ラムダ式を Expression 型の変数に代入すると、式木データとして扱うことができ、 以下のように式中の項を取り出したりといった操作が可能です。

Expression<Func<int, bool>> e = n => n > 0;
BinaryExpression lt = (BinaryExpression) e.Body;
ParameterExpression en = (ParameterExpression) lt.Left;
ConstantExpression zero = (ConstantExpression) lt.Right;

インタプリタ型の関数型言語には、実行コードとデータを区別しないものがあって、 ラムダ式をあるときには実行コードとして、またあるときにはデータとして利用するということができました。 C# では「実行コードとデータを区別しない」というわけにはいかないですし、 デリゲートに代入するか Expression 型に代入するかによってコンパイル結果を変えることで、 関数型言語と似たような動作を実現しています。

ただし、ラムダ式をデリゲートに代入する場合と違って、 式木には少し制約があります。 式木にできるのは、単文の({} を使わない)ラムダ式だけです。 以下の例では、1つ目のラムダ式はコンパイル可能ですが、2つ目はエラーになります。

// ↓ これは OK
Expression<Func<int, bool>> p = n => n > 0;

// ↓ これは「式木に変換できません」と怒られる
Expression<Func<int, int, int>> f =
  (x, y) =>
  {
    int sum = x + y;
    int prod = x * y;
    return sum * prod;
  };

要するに、単文で書けるものしか式木にできません。 したがって、四則演算やメソッドコールは式木にできるんですが、 for や while などの制御構文は式木にできません。 (Expression 型にも、for や while に相当するノードはない。)

ちなみに、LINQ to SQL では、 このラムダ式を式木として扱う機能を使って、LINQ クエリ式の条件式などを式木データとして受け取って、 それを SQL クエリに変換してデータベースに問い合わせをかけるというようなことをしているようです。

例えば、以下のようなクエリ式を書いたとすると、

var q =
  from c in db
  where c.City == "London"
  select new {c.City};

foreach (var city in q)
  ...

db.Where や db.Select では、 データベースサーバに対して以下のような SQL を発行するしくみになっています。

SELECT TOP 1 [t0].[City]
FROM [Customers] AS [t0]
WHERE [t0].[City] = @p0

こういう動作は、c.City == "London" の部分をデリゲート(要するに実行コード)として受け取っていてはできません。式木データとして受け取って、その中身を見ながら SQL 文を作ります。

初期化子

オブジェクトの初期化を以下のような記法でできるようになりました。 このような記法をオブジェクト初期化子(object initializer)と呼びます。

Point p = new Point{ X = 0, Y = 1 };

ちなみに、このコードの実行結果は以下のようなコードと等価です。

Point p = new Point();
p.X = 0;
p.Y = 1;

この等価なコードを見ればわかると思いますが、 オブジェクト初期化子で指定できるのは public なメンバー変数またはプロパティのみです。 (初期化子を書く場所によっては protected や internal も可。 とにかく、初期化子を書いた場所からアクセスできる変数・プロパティのみ。)

ただし、初期化子を使うと、 プロパティへの値の代入を単文で書けるようになります。 これで何が嬉しいかというと、クラスのメンバー変数の初期化や、extression tree への代入が可能になります。

// expression tree には単文のラムダ式しか代入できない。
Expression<Func<Point>> f = () => new Point { X = 0, Y = 0 };
class Triangle
{
  public Point A = new Point { X = 0, Y = 0 };
  public Point B = new Point { X = 1, Y = 0 };
  public Point C = new Point { X = 0, Y = 1 };
}
リスト初期化

また、コレクションの初期化を以下のような記法でできるようになりました。 こちらはコレクション初期化子(collection initializer)と呼びます。

List<int> list = new List<int> {1, 2, 3};

要するに、配列と同じような初期化記法を、任意のコレクションクラス(System.Collections.Generic.ICollection<T> インターフェースを実装するクラス)に対して行うことができます。 ちなみに、このコードは以下のようなコードと等価です。

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);

ICollection に加え、IDictionary<TKey,TValue> 実装クラスに対しても、 以下のような記法で初期化ができます。 (ICollection の場合と同様に、Add メソッドが呼ばれます。)

var map = Dictionary<string, int>
{
  { "One", 1 },
  { "Two", 2 },
  { "Three", 3 },
  { "Four", 4 },
};
再帰初期化

ちなみに、再帰的な構造を持ったクラスの初期化もできます。

using System.Collections.Generic;

class Point
{
    public double X { get; set; }
    public double Y { get; set; }
}

class Color
{
    public Byte R { get; set; }
    public Byte G { get; set; }
    public Byte B { get; set; }
}

class Geometry
{
    public List<Point> Vertices;
    public List<int> Indices;
}

class Model
{
    public Geometry Geometry { get; set; }
    public Color Color { get; set; }
}

class Program
{
    static void Main()
    {
        Model m = new Model
        {
            Color = { R = 128, G = 128, B = 128 },
            Geometry =
            {
                Vertices =
                {
                    new Point { X = 0, Y = 0},
                    new Point { X = 1, Y = 0},
                    new Point { X = 1, Y = 1},
                    new Point { X = 0, Y = 1},
                },
                Indices = { 0, 1, 2, 0, 2, 3 },
            },
        };
    }
}

ただし、再帰的な初期化をするためには、メンバーが参照型(class)である必要があります。 例えば、上記の例で、Color が class ではなく struct だった場合、 コンパイルエラーになります。

匿名型

C# 3.0 では匿名型(anonymous type)を作成できるようになりました。 匿名型の作り方は以下の通りです。

var x = new { FamilyName = "糸色", FirstName="望"};

このようなコードから、自動的に、以下のような型が生成されます。

// ↓この __Anonymous という名前はプログラマが参照できるわけではない。
class __Anonymous1
{
  private string f1;
  private string f2;

  public string FamilyName
  {
    get { return this.f1}
    set { this.f1 = value}
  };
  public string FirstName
  {
    get { return this.f2}
    set { this.f2 = value}
  };
}

そして、変数 x に対して、 2つのプロパティ FamilyName と FirstName が使えます。

var x = new { FamilyName = "糸色", FirstName="望"};

Console.Write("{0}\n", x.FamilyName, x.FirstName);

ちなみに、以下のように、他のクラスのプロパティを初期化子に渡す場合には、 「プロパティ =」の部分を省略することもできます。 (初期化子で渡したプロパティの名前がそのまま匿名クラスでも使われます。)

struct A
{
  public int X { set; get; }
  public int Y { set; get; }
  public int Z { set; get; }
}

class Program
{
  static void Main(string[] args)
  {
    A a = new A { X = 0, Y = 1, Z = 2};
    var b = new { a.X, a.Y };
    //↑ new { X = a.X, Y = a.Y } と同じ意味。
    Console.Write("{0}, {1}\n", b.X, b.Y);
  }
}

まあ、匿名クラスは、その場限りの使い捨てなクラスになるわけで、 普通はあまり使うような機能ではありません。 基本的には、LINQ のための機能だと思っていいでしょう。 例えば、後述するクエリ式中で、以下のように利用します。

var list1 =
  from p in list
  where p.id <= 15
  orderby p.id
  select new { p.FamilyName, p.FirstName };

暗黙型付け配列

new で配列を作成する際、 型を省略できるようになりました。

int[] array = new[] {1, 2, 3, 4};

見ての通り、 new の後ろの型を省略しています。 配列の型は、{} の中身の型から推定されます。 この例の場合、中身が 1, 2, 3, 4 といずれも int 型なので、 配列は int[] 型になります。

まあ、これだけだと、 ちょっとタイピングをサボれる程度ですが、 var および匿名型と組み合わせることによって、 真価が発揮されます。

var array = new[]
  {
    new {X =  0, Y =  1},
    new {X =  3, Y = -1},
    new {X =  7, Y =  3},
    new {X = 13, Y = -5},
  };

foreach(var p in array) Console.Write("{0}\n", p);

配列宣言の中身が匿名なんだから、 new の後ろにどういう型名を書いたらいいかわかるはずがないですからね。

[お問い合わせ](q)   ぷちカンパ