| |||
Hello, World | ||||||
なにはともあれ,最初に Hello Wolrd プログラムを見て頂こう.
|
これを見れば,C# のシンタクスがいかに Java に近いかがお分かり 頂けると思う.この短い Hello World プログラムに見られる C# の特徴として,以下が挙げられる.
|
言語目標 |
C# と Java が目指すゴールにも,多くの共通点がある. Java は,最初そのC++との関係から,別名 'C++--' とも言われた. C++から余分な(複雑なあるいはエラーを発生させやすい)機能を取り去ったという意味である. C# は,# が + 4つに見えることから,'C++++' の意味と解釈できる. あるいは,音程を示すC(ド)を半音上げたという意味かもしれない. このネーミングもやはり C++ を意識している. ここでは,C# のゴールを Java と比較してみる. |
C#/Java に共通するゴールC# と Java の両方に共通するゴールを挙げてみる.
|
C#/Java に共通しないゴール
|
様々な言語機能と共通点,相違点 | ||||||||||||||||||
次に,C# が提供する様々な言語仕様を一通り眺めていこう. Java との共通点,相違点の比較を行いながら各機能を紹介する. 対応するコードが書けるものはできるだけ両言語のコードを示した. C# のコードのみを記したものもある. | ||||||||||||||||||
ガベージコレクションC# では,C/C++ の様にヒープ(自由領域)に生成されたオブジェクトのメモリ解放をプログラマが行わなくて良い. これは,Java も同じである.ヒープに生成されたオブジェクトは, プログラムのどの部分からも参照されなくなった時点で,ガベージコレクション(ごみ集め)の対象となり, 時期を見て自動的に解放される. よく知られる通り,プログラマによる手動メモリ管理はバグを発生させる確率が非常に高い. ガベージコレクションにより,プログラマはメモリ管理から解放され, 本来の仕事であるビジネスロジックやドメインの問題に専念することができる. | ||||||||||||||||||
値型と参照型C# の変数の型はそのセマンティクスの違いから,「値型」と「参照型」の2つに分類される. また,定義が言語に組み込まれているかどうかによって, 「定義済型」(組み込み型)と,「ユーザ定義型」に分類される.これは Java と同様である.
値型の変数はスタック上にその領域が取られ,代入によって値がコピーされる. int, long, double といったおなじみの型は定義済の値型である. これに対して,string,object は定義済の参照型,ユーザ定義の構造体は値型, ユーザ定義のクラスは参照型である. 参照型の変数はヒープ上のオブジェクトを参照しており, 変数の代入時には参照先のオブジェクトが共有される. C#に特徴的な定義済の値型は,decimal という型である. 10進で28桁を正確に保持することができる.金融アプリケーションでの 大きな金額のような,整数の丸め誤差が許されない量を扱うのに適している. これは,RAD サポートを強く意識した仕様であろう. decimal income = 1.23M; // 1,230,000 強いて言えば,Java でこれに対応するものとして java.math.BigDecimal クラスを挙げることができる. また,C# 型システムの大きな特徴として,値型と参照型に共通の基底クラスが存在することが挙げられる. 値型も参照型も,すべての型はobject と呼ばれるクラスから派生している. よって,例えば int のリテラルでさえ,object に定義された ToString()メソッドを持っているし, object 型を引数に取るコレクションクラスに直接値を格納することも可能だ. using System; public class Stack { public object Pop() { ... } public void Push(object o) { ... } } class Test { static void Main() { Console.WriteLine(3.ToString()); // 整数リテラル 3 の ToString() メソッド object o = 2; // ボクシング(object に箱付め) int i = (int)o; // アンボクシング(object から値を復元) (i == 2) Stack s = new Stack(); s.Push(3); // 整数リテラル 3 を直接プッシュ i = (int)s.Pop(); // ポップすると値が復元 (i == 3) } } 値型を object に代入することをボクシング(boxing) - 箱詰め - と呼び, 逆に object をキャストして値型に戻すことをアンボクシング(unboxing)と呼ぶ. Java は,コレクションクラスを利用する際にint 型の代わりに Integer ラッパークラスを使った オブジェトを用いる必要がある. C# では上記に示すように,値型と参照型の型統一(type system unification)によって, 値型をそのままコレクションに入れることができる. |
クラスの継承とインターフェイスの継承まず,クラスやインターフェイスの継承に関する用語に注意する.C# はほとんど C++ と同じ用語を用いる.
C++ と異なり,C# ではクラスの単一継承しか許されない.しかし,Java と同様に,interface と呼ばれる実装やデータを持たないシグニチャの集合を 宣言でき,クラスはこれを継承することができる. |
C# |
public interface Scaleable { void Scale(int factor); } public class Shape { public virtual void Move(int x, int y) { // Shape クラスの move } } public class Circle: Shape, Scaleable { public void Scale(int factor) { // Scaleable インターフェイスの実装 } override public void Move(int x, int y) { // Shape の move をオーバーライド } } |
Java |
public interface Scaleable { void scale(int factor); } public class Shape { public void move(int x, int y) { // Shape クラスの move } } public class Circle extends Shape implements Scaleable { public void scale(int factor) { // Scaleable インターフェイスの実装 } public void move(int x, int y) { // Shape の move をオーバーライド } } |
両言語とも,インターフェイスとクラスの両方を継承することができる. ただし,Java ではインターフェイスは「継承する」と言わずに「実装する」と言う. Java では,クラスの継承は extends, インターフェイスの実装は implementsというそれぞれ別のキーワードを用いる. C# では,共にクラス名の後の":" に続けて書くことで,継承を意味する. また,C++ にあるような,privateや protected な継承はなく, 継承と言った場合はすべて public な継承である点も,Java と同じである. Java との大きな相違は,基底クラスのメソッドをオーバーライドする際には, 明示的な virtual/override キーワードが必要である点だ. これにより,次のような起こりがちなミスを防いでいる.
この override キーワードの機能は,おそらく Delphi から引き継いだものであろう. 基底クラスは明示的に virtual としてオーバーライドされる用意があることを示し, 派生クラスも明示的に override としてオーバーライドしているつもりであることを示す. この対応が崩れれば,コンパイラがエラーや警告としてプログラマに意図のずれを報告できる. | ||||
struct と enumC# は,Java には存在しない enum と struct をサポートしている.
enum は,種類を列挙する場合に便利な機能である. C++/C と同様,enum の各要素には,コンパイラにより自動的に整数に対応する数値が振られ, enum 値によって分岐が可能である.enum は,すべてSystem.Enum から派生した値型となる. Java ではenum の替わりにクラスやインターフェイスのpublic static final フィールドを使う などすることが多い.この場合,対応する整数値は自分で与える必要がある. C# では構造体(struct)は値型として扱われる. よって class を使うよりもメモリ効率が良く,特に大量のオブジェクトを生成する場合などに 利用される.Java にはこれに対応するものはない. 実は,C# で予約語になっている int という型は,System.Int32 構造体の別名となっている. |
print 文のフォーマットC# は,Java より少し分かりやすい print 文のフォーマット機能を有している. |
C# |
string name = "Aska"; int age = 5; Console.WriteLine("{0} is {1} years old.", name, age); |
Java |
String name = "Aska"; int age = 5; System.out.println(name + " is " + age + " years old."); |
C# の方が,全体の文章が見えて読みやすい.Java は文章が途切れ途切れに なる.C# は,後述する可変長引数の機能を使って,C 言語の printf 形式 に似たフォーマット記述が可能である.余談だが,言語が オブジェクト指向に近付くにつれて print 文の記述が見にくくなる, と指摘した方がいた. 読みやすさの順では,COBOL > C >> C++ > Java ではないだろうか. C# のフォーマットは,C 言語よりも見やすい. 多次元配列C# の配列のシンタクスは java とほぼ同じである.配列を組み合わせて, 「配列の配列」を作ることもできる.さらに,Javaにはない機能として, 多次元配列がサポートされている.これは,配列の添え字を","(カンマ) で区切ることにより,複数の添え字で要素にアクセスできる機能である. |
// C# でも Java でも正しいコード // 多次元配列の例 int [] a1 = new int[] { 1, 2, 3 }; // 1次元配列の生成と初期化 // 配列の配列も可能 int [][] aa = new int[3][]; aa[0] = new int[] {1, 2, 3}; aa[1] = new int[] {11, 12, 13, 14}; aa[2] = new int[] {21, 22, 23, 24, 25}; // ここからは,C# のみ正しいコード // 多次元配列の例 int [,] a2 = new int[3,3]; // 3x3 2次元配列の生成 int [,] a3 = new int[,] {{1, 2, 3}, {11, 12, 13}}; // 3x2 2次元配列の生成と初期化 |
メソッド引数C# のメソッドの仮引数には,
がある.値引数は通常の入力引数に使われるもので,引数の変更が メソッド呼び出し側に影響しない.Java の通常の引数と同じ である.参照引数は,引数の変更が呼び出し元に伝わる形式である. 出力引数は,参照引数とセマンティクスは同じだが, 引数を渡す時点で変数が初期化されていなくてもよいという特徴がある. class Test { // 交換(うそ) static void FakeSwap(int x, int y) { int t = x; x = y; y = t; } // 交換 static void Swap(ref int x, ref int y) { int t = x; x = y; y = t; } // 割算 static void Divide(int x, int y, out int ans, out int rem) { ans = x / y; // 商 rem = x % y; // 余 } // 可変長 static void Print(params int[] args) { for (int i = 0; i < args.Length; i++) Console.WriteLine("arg[{0}] = {1}", i, args[i]); } static void Main() { int a = 1; int b = 2; FakeSwap(a, b); // 何事も起らない(a == 1, b == 2) Swap(ref a, ref b); // 今度は交換(a == 2, b == 1) int c, d; Divide(10, 3, out c, out d); // c, d に値が入る(c == 3, d == 1) Print(a, b, c, d); // 全部の変数をプリント Print(new int[] {1, 2, 3}); // 配列をプリント } } 上記コードで特徴的なのは,引数のタイプによってメソッドを呼び出す側 が明示的に,ref, out というキーワードを付けて呼び出さなければならない 点である.これは,呼び出す側の勘違いを減らす利点がある. 可変長引数も,場合によっては便利なケースがある.例えば, Console.WriteLine が可変長引数を持つメソッドの代表例である. イベントとデレゲートC# はイベント伝達にユニークな手法を提供している.これは おそらく RAD を意識しながら Java のイベントモデルを 言語サポートしたもとの思われる.Delphi からも大きな影響が 見える.この手法によれば,Java のイベントモデルと同様のこと が,ライブラリやコーディングコンベンションではなく言語によって 強くサポートされる.デレゲートにより,C# では開発環境も含めた ビジュアル GUI プログラミングが促進されることになるだろう. delegate キーワードによって,「関数ポインタ」のような実行対象の カプセル化が可能になる.デザインパターン的にはCommandパターン と言って良いし,Java 風に言えばメソッド1つのみを持つ interface に対応する. delegate は,その実行対象がインスタンスメソッドであっても, クラスメソッドであっても,呼び出しシグニチャを満たすものであれば それをカプセル化することができる.デレゲートのインスタンスを 引き数として渡すことで,実行を委譲することが 可能になる(delegateは委譲と和訳されることが多い). この仕組みによって,GUI 部品であるボタンが押されたときの ような処理を,GUI 部品側とクライアント側にうまく分割することが できる.Java はイベントモデルによってこれを実現しているが, C# は一歩これを進めて言語仕様に取り込んでいるように見える. Java のイベントモデルに於けるイベントリスナー・インターフェイス の役割として,デレゲートを用いることができる. |
C# |
// GUI 部品側 // ボタンクラス public class Button { public event ButtonEventHandler Click; // イベントハンドラ // event には,自動的に delegate の取り外し // +=, -= 演算が定義されている. } // ボタンイベント public struct ButtonEvent { // .... } // デレゲートの宣言 public delegate void ButtonEventHandler(object sender, ButtonEvent e); // クライアントコード public class MyForm1 { // ボタンの生成 Button Button1 = new Button(); // ボタンが押された時のコールバック void Button1ClickCallback(object sender, Event e) { Console.WriteLine("Button1 がクリックされた"); } // コンストラクタ public MyForm1() { // コールバックメソッドの登録 Button1.Click += new ButtonEventHandler(Button1ClickCallback); } // イベント伝播の切断 public void Disconnect() { // コールバックメソッドの削除 Button1.Click -= new ButtonEventHandler(Button1ClickCallback); } } |
Java |
// GUI 部品側 // ボタンクラス public class Button { List listeners = new Vector(); void addButtonEventListener(ButtonEventListener l) { listeners.add(l); } void removeButtonEventListener(ButtonEventListener l) { listeners.remove(l); } } // ボタンイベント public class ButtonEvent { // .... } // ボタンイベントのリスナ・インターフェイス public interface ButtonEventListener { void buttonClicked(ButtonEvent e); } // クライアントコード public class MyForm1 implements ButtonEventListener { // ボタンの生成 Button button1 = new Button(); // ボタンが押された時のコールバック(リスナ・インターフェイスの実装) public void buttonClicked(ButtonEvent e) { System.out.println("Button1 がクリックされた"); } // コンストラクタ public MyForm1() { // コールバックメソッドの登録 button1.addButtonEventListener(this); } // イベント伝播の切断 public void disconnect() { // コールバックメソッドの削除 button1.removeButtonEventListener(this); } } |
C# では,イベントソース側は,デレゲート型のシグニチャ(ButtonEventHandler)を宣言し, Buttonクラスに event 型のデータメンバを用意する. リスナーの列を自前で保持する必要がなく,event 型の +=, -= 演算によってリスナの取り外しが行える. クライアント側(MyForm1 側)も,任意の名前のメソッドをコールバックに設定することができ, さらにリスナー・インターフェイスを実装する必要もない. Java に比べて,イベントソース側/リスナ側のいずれものコードがすっきりする. より簡便にイベントの授受を表現していると言えよう. さらに,Event の型を struct にすることによって,ガベージコレクトのオーバーヘッドも防ぐ, というパフォーマンス上の利点もある. プロパティC#には,プロパティとよばれる特殊なメンバがある. JavaBeans のプロパティと対応関係がある.イベント同様, Java の考え方を言語によってサポートしようとしている. |
C# |
// ボタンクラス public class Button { private string caption; public string Caption { get { return caption; } set { caption = value; Repaint(); } } } // クライアントコード Button b = new Button(); b.Caption = "Example Button"; // set が呼ばれる string caption = b.Caption; // get が呼ばれる b.Caption += "(1)"; // get/set が呼ばれる |
Java |
// ボタンクラス public class Button { private String caption; public String getCaption () { return caption; } public void setCaption(String value) { caption = value; repaint(); } } // クライアントコード Button b = new Button(); b.setCaption("Example Button"); String caption = b.getCaption(); b.setCaption(b.getCaption() += "(1)"); |
C# のプロパティは,クライアントからは public なインスタンス変数に見え, アクセスのためのシンタクスもインスタンス変数と同じだ. しかし,上記ボタンクラスの Caption のように,get/setと名前の付いたコードが書け, 自動的にこのコードが値の取得・設定時に呼び出される. set 時の仮引数は,value という名前に予約されている. JavaBeans においても,set/get メソッドの命名規則によりプロパティが定義されており, リフレクション機能を用いて実行時に外部からプロパティの一覧を取得することができる. C# では,これを一歩進め,言語によってプロパティをサポートしている.Visual Basic などでサポートされている機能であり,これも RAD を強く意識している. インデクサC# には,もう1つ,通常のメソッドとはシンタクスを異にするメソッド定義の機能がある. それは,添え字をサポートするもので,インデクサと呼ばれる. 上述のプロパティと少し似ており,get/set の組で定義を行う. |
C# |
// ベクトルクラス public class Vector { private object[] objs; // .... public Vector(int size) { objs = new objects[size]; } public object this[int index] { get { if (objs.Length <= index) { throw new Exception("Index out of range."); return objs[index]; } set { if (objs.Length <= index) { throw new Exception("Index out of range."); objs[index] = value; } } } // クライアントコード Vector v = new Vector(3); v[0] = 1; // set が呼ばれる v[1] = v[0] + 1; // get/set が呼ばれる v[1] == 2 v[1] += 2; // get/set が呼ばれる v[1] == 4 |
Java |
// ベクトルクラス public class Vector { private Object[] objs; // .... public Vector(int size) { objs = new Objects[size]; } public Object get(int index) { if (objs.length <= index) { throw new ArrayOutOfBoundException("Index out of range."); return objs[index]; } public void set(int index, Object o) { if (objs.length <= index) { throw new ArrayOutOfBoundException("Index out of range."); objs[index] = value; } } // クライアントコード Vector v = new Vector(3); v.set(0, new Integer(1)); // v[0] = 1 v.set(1, new Integer((Integer)(v.get(0)).intValue()+1)); // v[1] = v[0] + 1 v.set(1, new Integer((Integer)(v.get(1)).intValue()+2)); // v[1] += 2; |
インデクサを使うと,オブジェクトに対する添え字シンタクスがサポートされる. 場合によってはクライアントコードの可読性を向上することができる. C# では,int などの値型も object クラスから派生しているため, 特にコンテナ系のクラスではインデクサと共に利用することでクライアントコードが大幅に読みやすくなる. マルチスレッドC# でも,マルチスレッドは言語に取り入れられている. Java の synchronized に良く似た lock キーワードが定義されており, 次のような使用が可能である.
クラスコンストラクタC# には,通常のコンストラクタ(インスタンスコンストラクタと呼ばれる)以外に, クラスコンストラクタと呼ばれるメソッドが定義できる. これは,クラスが最初にロードされる際に1度だけ実行されるメソッドであり, Java の static イニシャライザに相当する. |
C# | Java |
class MyClass { static MyClass() { //...クラスロード時に一度だけ走るコード } public MyClass() { //...インスタンス生成時に走るコード } } |
class MyClass { static { //...クラスロード時に一度だけ走るコード } public MyClass() { //...インスタンス生成時に走るコード } } |
共に機能は同じだが,C#の方がネーミングとシンタクスに一貫性が あるかもしれない. unsafe コードC# では,C 言語を直接 C# 中に埋め込むことができる.これは, 丁度 C 言語がアセンブリ言語を asm キーワードによって インラインに埋め込めるのと似ている. 下記のコードは,配列要素のアドレスを書き出すものだ. class UnsafeTest { unsafe static void PrintAddress(byte[] array) { fixed (byte *p_array = array) { byte *p_elem = p_array; for (int i = 0; i < arr.Length; i++) { byte value = *p_elem; string addr = int.Format((int) p_elem, "X"); Console.WriteLine("array[{0}] at 0x{1} is {2}", i, addr, value); p_elem++; } } } } Java では,このようなネイティブなコードを呼び出す場合, JNI(Java Native Interface)を使う必要がある. C コードを直接C# テキスト中に埋め込むことができる, という言語設計は,Java とは全く違ったコンセプトである. |
おわりに |
C# の言語仕様を,Java と比較しながら駆け足で紹介した. ここで紹介できなかった他の機能に,クラスのオペレータ多重定義機能, クラスに URL を結び付け,それを実行時に取り出せる attribute 機能などが用意されている. これらは Javaには無い機能である. また,C++ にある template 機能は,C# でも Java 同様用意されていない. C# の言語仕様を概観すると,それが Java の仕様,および JavaBeansをはじめとする Java の周辺仕様をうまく取り入れ,言語としてより成熟したものとなっている点が評価される. 筆者は特にRAD を推進する機能として, デレゲートとプロパティを言語仕様として取り込んでいることを評価している. しかし,これだけ Java と似た言語が, 現在のプログラマに受け入れられるかどうかは定かではない. おそらく,開発環境および.NET との組で考えなければならない言語であることは間違いがなさそうである. |
参照文献 |
|
謝辞 |
この記事の構想および洗練に多くの方々からコメントとヒントを頂いた. 特に, 畑匠美さん,海内利之さん,矢崎博英さん, 上手裕さん, 森下民平さん, 友野晶夫さん, 山田健志さん, 藤本聖さん, 岡村敏弘さん, 瀬戸川教彦さん, 名須川竜太さん, 小野修司さん, 中村正さん, に感謝します.ありがとうございました. |
Kenji Hiranabe <hiranabe@esm.co.jp>
Last modified: Sun Nov 12 18:39:03 2000
|