SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

言語仕様から読み解くC# 3.0入門

C# 3.0で追加された言語仕様の解説

新しいスタイル-動的さと不変

動的と静的

 C#は相変わらず静的型付言語に部類される言語ですが、うまい具合に動的型付言語臭を漂わせているように思います。これまでの例の中でもそのような部分を見ることができました。多少憶測や雑感を含んでいる点もありますが、そう思うに至った点を紹介していきます。

 まずは、やはりvarによるローカル変数の宣言だと思います。今ここで「コーディング時」「コンパイル時」「実行時」という大きく分けた3つのフェーズを考えます。従来だと型はコーディング時に確定している必要がありました。しかし、C# 3.0では型推論のおかげで型の判断をコンパイル時にまで遅らせることが可能となります。その結果、型を意識することなく今までよりも柔軟にコードを書ける場面が出てきます。

 クロージャの紹介で挙げた例をもう一度載せます。この例はクロージャに型情報を保持させて、必要なときにインスタンスを作成するというものでした。

public class HogeA
{
    public void Hello()
    {
        Console.WriteLine( "hello" );
    }
}
public class MainClass
{
    public void MainFunction()
    {
        var f = Foo<HogeA>();
        var instance = f();
        instance.Hello();
    }
    private Func<T> Foo<T>() where T : new()
    {
        return ( () => new T() );
    }
}

 例ではvarを2箇所で使っていますが、使っていなかった場合はどうなるでしょうか。

こうなる?(一部抜粋)
Func<HogeA> f = Foo<HogeA>();
HogeA instance = f();
instance.Hello();

 おそらくこのように書くはずです。ここで仮に、Hello()という関数を持つHogeBクラスがあり、Fooに渡す型をHogeBに直す場合を考えます。すると、前者の修正は1箇所で済むのに対し、後者では3箇所の修正が必要になります。厳密な型の記述をコンパイラに任せることによって、型に強く依存することなく共通部分を記述できるという点ではGenericsに似ているかもしれません。

 また、HogeAHogeBに共通のインターフェースはありませんが、そのクラスがHello()という関数を持っていると事前に分かっているのであれば、他の部分を修正することなく、そのクラスをFooに渡すことができます。このことから「静的Duck Typingもどき」とも言えるかもしれません。

 動的型付言語まで柔軟ではありませんし、状況が限られてはいますが、var1つを見ても今までと違ったスタイルでプログラムを組むことができます。

 var以外にも、匿名型やラムダ式(匿名メソッド)を使えば、その場で使い捨てクラスを用意したり、関数の中で関数を作ったりと、動的な感覚を味わうことができます。これは結局コンパイラが一生懸命展開しているだけですが、そのおかげでプログラマは動的な香りを楽しみつつ、静的型付言語C#でプログラミングできるようになります。

関数プログラミングを取り入れる際の注意点

 ラムダ式等の導入により、関数プログラミングをより直感的に行うことができるようになりました(もちろん今までが不可能だったわけではありません)。しかし、関数プログラミングの根底には「変数の値は不変である」という考え方があります(手続き型でもImmutableパターンなどのようなものもありますが)。

 関数型言語の中にも値の変更が可能なものもありますが、それらの操作は「破壊的操作」と呼ばれるほどです(むしろ、Lispのような値を変更することも可能な言語は、厳密には関数型言語とは言えないみたいです)。従来のC#君はどうだったかと言うと、関数型言語ちゃんから見れば傍若無人に破壊しまくる人だったわけです。

 言語設計者であるヘルスバーグ氏的には、C#君と関数型言語ちゃんに幸せな結婚をして欲しいのでしょうし、現に彼はうまくふたりの仲を近づけているようにも思います。しかしここでプログラマが誤ったコードを書けば、ふたりの仲は険悪なものとなってしまいます。お互いの悪い部分までをも十分に理解し、良い部分を尊重しあって幸せな家庭を築いていきましょう。

C#での関数プログラミング

 一体何の記事なのか分からなくなって来ましたので、この辺で現実に戻って注意点について見ていこうと思います。では早速、C#に関数プログラミングを取り入れる際の注意点について解説していきたいと思います。

 先ほど述べたように、関数プログラミングでは変数の値を変更するというのはそんなに多いことではなく(or まったくなく)、基本的に不変であることを前提にプログラムを書き進めていきます。そのため、途中で勝手に環境を変えられると予期せぬ結果を招くことに繋がります。破壊することが日常茶飯事だったC#君と結婚するためには、まずここに気をつける必要があります。

 では早速、プログラマの書いたコードによってふたりの間にすれ違いが生じてしまった悲しい結末を見てみましょう。

public void Happy()
{
    int a = 10;
    int b = 10;
    // 変数の初期の状態を使って計算する関数を作っておく
    Func<int> fn = ( () => a * b );

    b = 0;
    this.Foo( fn ); //最初の状態での計算結果を出力しておくれ!
}
private void Foo( Func<int> fn )
{
    Console.WriteLine( fn() );
}
結果
0

 10 * 10で100と表示されるかと思いきや、10 * 0によって出力は0となります。fnはあくまでHappy関数内の環境、つまり変数aと変数bを掛け合わせる関数であって、10 * 10という式を内部に保存しているわけではありません。途中で破壊的操作が入り込めば結果も変わってしまいます。

 今回のこれはクロージャの作り方によっては回避できる問題です。しかし、参照型になると(この例はintなので値型)状況はさらに悪化します。

 ですが、もともと違う道を歩んできたわけですから、このような食い違いは当然と言えば当然なのかもしれません。「気をつけるべき点」の1つとして認識したうえで、C#により関数プログラミングを行うことが求められます。関数プログラミングを真似たテクニックは、少なくとも現段階では、従来のC#のコード部分とある程度分離して用いるのが無難かと思います。

 C#は、おそらく今後進化する過程でより関数型言語の影響を受けていくことと思いますが、その点からすればC# 3.0はまだ始まりであって、C#による関数プログラミングのスタイルを確立できたわけではありません。そもそも手続き型と関数型がごちゃ混ぜになったプログラムは、おそらく読むのが不快になります。C#は関数型言語になったわけでもないですし、もともと持つ良い点がたくさんあります。「ここは関数プログラミングを用いればより直感的に美しく書ける!」、そんな時に関数プログラミングのテクニックを用いましょう。適材適所であると思います。

閉ざされた環境

 さて、例に戻ります。関数プログラミングを行う以上、不変(immutable)を意識する必要がありますが、その他の解決策も考えてみることにしましょう。

 上の例の場合、クロージャ作成時の変数(ab)の値が欲しいが、変数の値は変化する可能性がある、という問題を持っています。おそらくこの場合最も簡単なのは複製を用意することです。値型であれば、クロージャを生成する関数を定義しておけば引数として渡されたものは結果的にコピーとなりますし、参照型の場合は内部でCloneを作って保持しても良いでしょう(そのような環境にアクセスする手段は通常ありませんので不変を保てます)。

 クロージャ作成関数の中で複製を用意するのか、また関数を呼び出す側で複製を前もって準備しておくのかは自由ですが、混乱を避けるためにもどちらか一方に統一する必要はあります。

 以下に複製を利用した例を示します。この例では関数内で複製を用意し、クロージャはそれを環境とするようになっています。

public void Happy2()
{
    List<int> list = new List<int>{ 7, 3 };
    var fn1 = this.CreateClosure_bad( list );
    var fn2 = this.CreateClosure( list );

    list[ 0 ] = 95;
    list[ 1 ] = 5;

    Console.WriteLine( fn1() );
    Console.WriteLine( fn2() );
}

private Func<int> CreateClosure_bad( List<int> list )
{
    return ( () => list[ 0 ] + list[ 1 ] );
}
private Func<int> CreateClosure( List<int> list )
{
    var clone = list.ToList();
    return ( () => clone[ 0 ] + clone[ 1 ] );
}
結果
100
10

 Lispも関数型言語の機能を持った言語ですので、このような問題が起こらないわけではありません。しかし、変数の値を変える機会は全体からすれば少ないですし、関数型であることが意識されているため、このような問題にはそこまで遭遇しません(言語初心者なら遭遇しまくるかもしれませんが…)。Lispでは「ここでは変数の値を書き換えた方が、より直感的だ」とか、速度を求められる部分での効率のことを考えた上で破壊的操作が行われます(とはいえ、最終的には書く人の自由ですが)。関数プログラミングも可能でかつ破壊的操作も可能なC# 3.0はLispに学ぶべき点があるかもしれません。

 実はC#3.0で追加された「匿名型」も最初は値の変更を許可していたようですが、のちに不変を保つよう仕様が変更されています。

まとめ

 今回はあえてLINQの話はほとんど出さず、純粋にC# 3.0で遊んでみました。筆者自身大変未熟ですし入門を意識した記事なので浅い内容のことしか書けませんでしたが、何か新しい発見でもあったなら幸いです。関数プログラミングはあくまでパラダイムなので、まったくそれを用いずにC# 3.0でプログラミングすることももちろん可能です。しかし、最後に扱った「関数プログラミングを取り入れる際の注意点」などは、LINQを使うのであれば意識する必要があるかと思います。

 C# 3.0の解説はちらほら見かけますが、関数プログラミングの例などを扱っているものが少なかったので、何となく書いてしまいました。プログラミング暦(特に関数型言語やLispの知識)が浅い未熟者の記事なので間違いが大量にあるんではないかと不安ですが、気になった点がありましたら教えてやってください。また、「C#でこんなクレイジーな関数プログラミングしちゃったぜ!」なんて方はぜひトラックバックしてください。勉強させていただきます。

参考資料

  • C# 3.0 言語仕様書(Wordドキュメント)
  • 正式な仕様書はまだリリースされていない模様です。 ここに挙げたのは日本語で読めるものですが、少し古い点もあります。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

のぶひさ(ノブヒサ)

1987年10月10日生まれ札幌市在住参加コミュニティ:CLR/Hはてなダイアリー:http://d.hatena.ne.jp/Nobuhisa/ 

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/1771 2007/10/30 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー