C++プログラマ向けC#ひとめぐり
この文章は先日中途で入社されたSさんに向けて書いています。SさんはC++とJavaの経験はあるが、C#の経験はないそうです。
という事情でして、C++やJavaと対比しながらC#を説明すれば手っ取り早くC#を覚えて頂けるかな、などと思うわけです。しかしながら私自身C++は最近書いてないし、Javaに至っては10年以上前に少し触ったことがあるだけ、という状態。とりあえずJavaとの比較は諦めます。C++についても全くもって正確な記事が書ける自信がないことをお断りするとともに、間違ってたらぜひツッコミよろしくおねがいします><
(あ、あと C++11 は分からないので、C++11 以前の C++ を前提に書いています。SさんもC++11に詳しいわけでは無さそうですし…)
class と struct
C++ では class と sturct に本質的な違いがなく、単にメンバがデフォルトで public か private かの違いしかありません。しかし C# では class と struct は全く違うものです。
class Hoge { ... }; Hoge hoge; // スタックに積まれる Hoge* ptr = new Hoge(); // ヒープに生成される Hoge hoge2 = hoge; // 値がコピーされる(コピーコンストラクタ/コピー演算子) Hoge* ptr2 = ptr; // ポインタのコピー
class Hoge { ... } struct Piyo { ... } Hoge hoge = new Hoge(); // class はヒープに生成される Piyo piyo = new Piyo(); // struct はスタックに積まれる Hoge hoge2 = hoge; // 参照のコピー Piyo piyo2 = piyo; // 値のコピー
C++とC#で同じキーワードを微妙に違う意味で使っているので混乱を誘いますね(^^;
C# では new と書いてあっても struct ならスタックに積まれます。
const と readonly
優れたC++プログラマは丁寧に const を付けますね。C#にも const キーワードは存在しますが、C++とは少し意味が違います。また、一部は readonly というキーワードに置き換えられています。
class Hoge { const int A = 123; // 整数などの数値は定数を定義できます const string B = "Hello"; // 文字列も定数に出来ます readonly string C; // コンストラクタで初期化されたらそれ以降は読み取り専用となります public Hoge( string c ) { this.C = c; } }
- C++ の const は「読み取り専用」を意味しますが、C# の const は「定数」です。システム全体を通して変化しない値です。
- クラスのメンバ(フィールド)を読み取り専用にしたい時は、const の代わりに readonly を使います。
- C++ では関数の引数やメンバ関数に const 属性を付けられますが、C# ではそういったことは出来ません。
クラスのフィールドは可能な限り readonly にする習慣をつけると良いと思います。
継承について
struct は継承できません。
class は継承できますが、多重継承は出来ません。その代わりに(?)インターフェイスは複数実装できます。このへんは Java と同じです。ですのでダイヤモンド継承とかバーチャル継承みたいな闇(…おっと誰か来たようだ)は存在しません。
コレクション(コンテナ)
C++のSTLで提供されているコンテナに(無理矢理)C#の(というより.NET Frameworkの)コレクションを対応付けてみました。
C++ | C# |
---|---|
配列 | T[] |
vector<T> | List<T> |
list<T> | LinkedList<T> |
set<T> | HashSet<T> |
map<T,U> | Dictionary<T,U> |
- これらは System.Collections.Generic 名前空間に定義されています。
- System.Collections 直下にあるクラスは黒歴史なので使わないで下さい。
- C# の List と C++ の std::list を混同しないように注意して下さい。
- C++ の set/map は二分木によるものですが、C# の HashSet や Dictionary はハッシュテーブルによるものですので、正確にはこれらは異なるものです。そういう意味では対応づけるべきではないのかもしれませんが、用途としては似ている場面が多いと思いますので上のような表にしました。
- 正直なところ、C++STLの方が自由度が高く高機能かと思います。
IEnumerable インターフェイス
これはとても大事なインターフェイスで、いずれ覚えて欲しい LINQ という機能にも関連してきます。
C++のSTLコンテナでは、コンテナ要素にアクセスするための統一的な方法として iterator を提供しています。C#では(.NETでは)この IEnumerable インターフェイスがコレクション要素にアクセスするための統一的な機能として用意されています。
C++ | C# |
---|---|
iterator で列挙 | IEnumerable で列挙 |
全てのコンテナは begin(), end() を提供 | 全てのコレクションは IEnumerable<T> を実装 |
ContainerType c;
for (ContainerType::iterator it = c.begin(); it != c.end(); ++it) {
...
}
CollectionType c; foreach (var item in c) { ... }
IDisposable インターフェイス
C++ ではスコープを抜けるときに必ず変数のデストラクタが呼ばれます。ですので、これを利用してデストラクタでリソースの解放処理を行うことがよくあります。例えばファイル操作などでは、スコープを抜けるときにデストラクタで確実にストリームを close() するといったパターンです。
{ std::ifstream fin("test.txt"); ... } // スコープを抜けるときに close される
しかし C# ではクラスのインスタンスはすべてヒープに確保されますし、メモリ等の解放処理はガベージコレクタが自動で行いますのでデストラクタのタイミングを制御することが出来ません。ですのでC++のようにデストラクタによってスコープ離脱時の解放処理を行うことはC#では出来ません。
その代わりとして、C#には using キーワードと IDisposable インターフェイスが用意されています。
using ( var reader = new System.IO.StreamReader( "test.txt" ) ) { ... } // スコープを抜けるときに Dispose() が呼ばれる
using の中に IDisposable インターフェイスを実装したオブジェクトを宣言すると、そのスコープを抜けるときに確実に Dispose() メソッドが呼び出されるようになります。
ちなみに、自作クラスに真面目に IDisposable インターフェイスを実装しようとすると割とややこしいことになります。「C# Dispose パターン」とか「C# Dispose Finalize パターン」とかでググると情報が出てくると思いますので、興味がありましたら調べてみてください。
yield return
だんだんC++には無い概念の説明に入っていきます。
yield return はちょっと分かりにくい概念で、私も最初は理解に苦労した記憶があります。この場で簡単に説明しただけでサクッと理解できるようなものではないと思いますので、いずれウェブなり書籍なりできちんと学習して頂ければと思います。ここではとりあえず
yield return を使うと IEnumerable<T> インターフェイスを真面目に実装しなくても簡単に IEnumerable<T> なオブジェクトが生成できる
ということだけ覚えて下さい。以下にFizzBuzz問題を例に取ったコード例を示します。
static IEnumerable<string> FizzBuzz() { for ( int i = 0; true; ++i ) { if ( i % 3 == 0 && i % 5 == 0 ) yield return "Fizz Buzz"; else if ( i % 3 == 0 ) yield return "Fizz"; else if ( i % 5 == 0 ) yield return "Buzz"; else yield return i.ToString(); } } foreach ( string s in FizzBuzz() ) Console.WriteLine( s );
この例では延々と無限に Fizz Buzz を出力し続けます。
どうです?とっても不思議な気がしませんか?
ヒントとしては、まず FizzBuzz() は「普通の」関数ではありません。関数に yield return が含まれていると、C# コンパイラが「普通とは違う扱い」をします。FizzBuzz() は関数のような顔をしていますが、C#コンパイラは内部で IEnumerable<T> を実装したクラスを生成しているのです。
ちなみに余談ですが、私は最初 yield の意味もしっくり来ず、これも理解の妨げになった気がします。英和辞典を引いてもしっくり来る意味が載っていないのです。英英辞典を調べてようやく腑に落ちました。
http://www.ldoceonline.com/dictionary/yield_1
- to produce a result, answer, or piece of information:
直訳すれば「結果とか答えとか情報とかを生み出すこと」でしょうか。
var と型推論
C++でもC++11からautoキーワードが導入されましたのでご存じかもしれません。
// 分かりきっている型を2回書く必要があり冗長 VeryLongLongNameClass a = new VeryLongLongNameClass(); // var キーワードを使って冗長な記述を排除できます // (型推論により型情報は失われておらず、b はVeryLongLongName型) var b = new VeryLongLongNameClass(); // これは型情報が失われているのでvarとは全く違います object c = new VeryLongLongNameClass();
var は人によって賛否が別れるところがあり、型名を省くと読みにくくなるという理由で使用を制限するところもあるようです。うちの会社ではそういった制限はしておらず、var は使えるところではどんどん使えばいいと思っています。
(とはいえ、さすがに int はタイプ数が変わりませんから int と書きますけど(^^;)
ラムダ式(とデリゲート)
C++ですと関数ポインタとか関数オブジェクト(ファンクタ)が比較的近い概念かなと思います。ここでは Array の Find() 関数を例に説明します。Find()は配列から最初に条件に合致する要素を返す関数で、C++の std::find_if() とよく似ているので分かりやすいでしょう。
// C# の Array.Find() public static T Array.Find<T>(T[] array, Predicate<T> match)
// C++ の std::find_if() template<class InputIterator, class UnaryPredicate> InputIterator find_if ( InputIterator first, InputIterator last, UnaryPredicate pred)
Array.Find() を使って int の配列から最初の偶数値を取り出すコードを書いてみます。
static bool IsEven( int i ) { return i % 2 == 0; } ... var array = new[] { 1, 3, 7, 6, 4, 2, 9 }; int a = Array.Find( array, IsEven ); // 関数を渡す int b = Array.Find( array, delegate( int i ) { return i % 2 == 0; } ); // デリゲートによる無名関数 int c = Array.Find( array, ( int i ) => i % 2 == 0 ); // ラムダ式 int d = Array.Find( array, i => i % 2 == 0 ); // ラムダ式(型推論により型を省略)
4通りの書き方を示しましたが、一番下が最も簡潔ですね。つまりこの書き方を推奨します。
delegate は使わないで下さい。delegate はラムダ式が C# 3.0 で導入される前の名残りだと思って差し支えないです。
LINQ
いよいよLINQですが、本題に入る前に一点野暮な注意をしておかねばなりません。"LINQ" でググると "LinQ" というアイドルグループがヒットしますが、全く関係ありませんので惑わされないように!
さて、LINQとは Language Integrated Query の略で「リンク」と発音します。LINQ には次の3種類があります。
これらのうち、ウチの業務で使用しているのは LINQ to Objects のみです。業種上あまりデータベースなどを扱うことが少ないため、LINQ to SQL などは利用する機会がなく私も使用経験がないので全く説明できません。ですので、ググると LINQ to SQL 等の説明も出てきますが、LINQ to Objects のみを学習して頂ければ十分ですのでご注意下さい。
何はともあれ、まずコード例を見てみましょう。見ても最初はわからないと思いますが、ザックリと雰囲気を見て頂ければ。
using System.Linq; IEnumerable<int> sequence = new[] { 1, 3, 7, 6, 4, 2, 9 }; // (A) メソッドチェイン形式 var q1 = sequence .Where( i => i % 2 == 0 ).OrderBy( i => i ).Select( i => i % 3 ); // (B) クエリ式 var q2 = from i in sequence where i % 2 == 0 orderby i select i % 3;
(A)(B)どちらも、「sequence から偶数のみを取り出し、昇順に並べ替えて、3で割った余りに変換」を表しています。(あまり意味がない処理ですが、簡潔で良い例が思いつかず…)
「なんとなくSQLっぽい」という雰囲気は伝わるでしょうか。LINQというのはデータソース(この場合は IEnumerable<int>型の sequence)に対して発行するクエリという概念を言語に組み込んでしまおう(Language Integrated)というコンセプトなのです。LINQ to SQL においては「まさにクエリ」という感じなのでしょうけど、上で書いたようにウチの業務ではデータベースはやりませんからクエリというのは直観に合いません。「クエリ」という言葉に惑わされないようにして下さい。単に IEnumerable<T> で表される列を操作するための機能セットです。
(B)のクエリ式というのは、LINQ専用に C# に組み込まれた特別な文法です。これぞ "Language Integrated" という感じですね。しかし、私はこのクエリ式を使うことは殆どなく、ほぼ100% (A)のメソッドチェイン形式を使っています。クエリ式はあまりにSQLに寄せすぎていての他の文法から浮いているように感じますし、結局のところメソッドチェイン形式のほうが汎用性が高いのです。ですから (B) は覚えなくていいです。
…ということはですね、覚えて欲しい部分は
- データベースはやらないので「クエリ」ではない
- クエリ式は使わないので "Languate Integrated ではない
というわけで「もはやLINQではない…」という感じなのですが、そうはいいつつ using System.Linq は必要ですし他に呼びようもないので仕方なく LINQ と呼んでいます。
では改めて (A) を見てみましょう。理解するためのポイントを幾つか挙げておきます。
- 既に上で説明した「ラムダ式」が幾つか見つかりますね。ラムダ式あってのLINQ、LINQあってのラムダ式。この2つは切り離せません。
- Where(), OrderBy(), Select() はいずれも「IEnumerable<T> を受け取って IEnumerable<T> を返す関数」です。
- Where(), OrderBy(), Select() は「拡張メソッド」として定義されています。拡張メソッドについても調べてみましょう。
到底全ては説明しきれませんが、「ハマりやすい落とし穴」を避ける情報は伝えたつもりです。あとはウェブや書籍で学習してみてください。