JavaやC++などのオブジェクト指向言語の経験者が、C#を使い始めて「Java/C++のアレはどうやって書くんだっけ?」と思いそうなことを整理してます。
C#4.0までの内容がメインです。末尾の文献もぜひ参考にしてください。
演算子
論理演算子と条件演算子
論理演算子(|はOR、&はAND)、条件演算子(||はOR、&&はAND)はいずれも論理式を評価して、trueまたはfalseの結果を返す。
違いは、論理演算子の場合は左式・右式の両方を評価するが、条件演算子の場合は左式の評価によって結果が確定すると右式を評価しない点にある。したがって、a || bでaがtrueとなったり、a && bでaがfalseになった場合は、bは評価されない。
int x = 0;
bool result = false & ++x == 1;
System.Console.WriteLine("result: " + result + ", x: " + x); // result: False, x: 1
result = true | ++x == 2;
System.Console.WriteLine("result: " + result + ", x: " + x); // result: True, x: 2
result = false && ++x == 1;
System.Console.WriteLine("result: " + result + ", x: " + x); // result: False, x: 2(++xは評価されない)
result = true || ++x == 1;
System.Console.WriteLine("result: " + result + ", x: " + x); // result: True, x: 2(++xは評価されない)
isで互換性がある型かどうかを判定
public class TestClass
{
class SuperClass { }
class SubClass : SuperClass { }
public static void Main()
{
SuperClass super = new SuperClass();
SubClass sub = new SubClass();
System.Console.WriteLine(super is SuperClass); // True
System.Console.WriteLine(super is SubClass); // False
System.Console.WriteLine(sub is SuperClass); // True
System.Console.WriteLine(sub is SubClass); // True
}
}
asでキャスト失敗時に例外を発生させない
public class TestClass
{
class SuperClass { }
class SubClass : SuperClass { }
public static void Main()
{
SuperClass super = new SuperClass() as SuperClass;
if (super != null) System.Console.WriteLine("SuperClass"); // SuperClass
super = new SuperClass() as SubClass;
if (super == null) System.Console.WriteLine("null"); // null
}
}
null許容型と関係演算子・論理演算子
null許容型にでは関係演算子・論理演算子の振る舞いに注意する必要がある。
- 関係演算子(<、>)では、一方がnullだと、評価結果はFalse
- 論理演算子では、一方がnullだと、
- OR演算子(|)では、もう一方がTrueなら式全体の評価もTrue、Falseならnull
- AND演算子(&)では、もう一方がFalseなら式全体の評価もFalse、Trueならnull
- NOT演算子(!)は、常にnull
int? a = 100;
int? b = null;
System.Console.WriteLine(a < b); // False
System.Console.WriteLine(a > b); // False
bool? x = true;
bool? y = false;
bool? z = null;
System.Console.WriteLine(x | z); // True
System.Console.WriteLine(y | z); // nullのため何も表示されない
System.Console.WriteLine(x & z); // False
System.Console.WriteLine(y & z); // nullのため何も表示されない
System.Console.WriteLine(!z); // nullのため何も表示されない
カンマ(,)演算子で式を逐次実行できない
x = (y = 10, z = 20, y + z); // コンパイルエラー
x = (y = 10) + (z = 20); // こっちはOK
制御構文、例外
switch文のフォールスルーは不可
switch(ch)
{
case 'A':
System.Console.WriteLine("A");
// break文が無いためコンパイルエラー
case 'B':
System.Console.WriteLine("B");
}
例外の再スローはthrow;でOK
public class TestClass
{
static double div(int a, int b)
{
try
{
return a / b;
}
catch (DivideByZeroException e)
{
System.Console.WriteLine("divでcatch: " + e.Message + ", " + e.TargetSite);
throw;
}
}
public static void Main()
{
try
{
double x = div(1, 0);
}
catch (DivideByZeroException e)
{
System.Console.WriteLine("Mainでcatch: " + e.Message + ", " + e.TargetSite);
}
}
}
checkedでオーバーフロー例外のスロー
uncheckedでスローさせないこともできる。
public class TestClass
{
public static void Main()
{
int a = 10000000, b = 10000000;
int x;
try
{
checked { x = a * b; }
System.Console.WriteLine("例外がスローされるのでこの文は実行されない");
}
catch (OverflowException e)
{
System.Console.WriteLine(e.Message);
}
try
{
unchecked { x = a * b; }
}
catch (OverflowException e)
{
System.Console.WriteLine("例外がスローされないのでこの文は実行されない");
}
}
}
配列
配列の初期化
ブラケットで初期値を代入できる。ブラケット内は末尾がカンマでもOK。
string[] prefectures = { "Tokyo", "Chiba", "Saitama", "Kanagawa", };
Member[] members = {
new Member(name: "Kazumi", age: 40), // 名前付き引数指定
new Member(name: "Nijiko", age: 40),
new Member(name: "Makoto", age: 16),
new Member(name: "Miyuki", age: 4),
};
多次元配列
int[,] table = {
{1, 2, 3, 4, 5},
{2, 4, 6, 8, 10},
{3, 6, 9, 12, 15},
{4, 8, 12, 16, 20},
{5, 10, 15, 20, 25},
};
System.Console.WriteLine(table[1, 3]); // 8
可変長引数
1つしか宣言できず、実引数リストの末尾に記述する必要がある。
public class TestClass
{
static void showAverage(string name, params int scores[])
{
int total = 0;
for (int i = 0; i < scores.Length; i++) total += scores[i];
System.Console.WriteLine("{0}: {1}", name, total / scores.Length);
}
public static void Main()
{
showAverage("Kazumi", 90, 100, 75, 80, 65);
}
}
クラス
アクセス修飾子が無いメンバはprivate
public class TestClass
{
public class TextTestClass
{
string text { get; set; }
}
public static void Main()
{
var c = new TextTestClass();
c.text = "test"; // コンパイルエラー
}
}
デストラクタを定義できる
デストラクタはガベージコレクション時(参照が無くなった時ではない)に呼び出される。アンマネージコードで確保した領域の開放などで使う。
public class TestClass
{
~TestClass()
{
System.Console.WriteLine("GC時に呼び出される");
}
}
別のコンストラクタの呼び出し
TestClass(string n)、TestClass(string n, int a)の順で実行される。
public class TestClass
{
private string name;
private int age;
public TestClass(string n)
{
name = n;
}
public TestClass(string n, int a) : this(n)
{
age = a;
}
}
親クラスのコンストラクタの呼び出し
public class SuperClass
{
private string name;
private int age;
public SuperClass(string n, int a)
{
name = n;
age = a;
}
}
public class SubClass : SuperClass
{
private string tel;
public SubClass(string n, int a, string t) : base(n, a)
{
tel = t;
}
}
baseで親クラスの同名フィールドへアクセス
public class SuperClass
{
public void Greeting()
{
System.Console.WriteLine("Hello!");
}
}
public class SubClass : SuperClass
{
public void Greeting()
{
base.Greeting();
System.Console.WriteLine("How are you?");
}
}
継承させないときはsealed
sealed public class SuperClass
{ }
public class SubClass : SuperClass // コンパイルエラー
{ }
static classでシングルトンクラス
static class SingletonClass
{
public static string Name;
public static void Greeting()
{
Console.WriteLine("Hello, " + Name);
}
SingletonClass() { } // コンパイルエラー(コンストラクタを定義できない)
}
public static void Main()
{
SingletonClass.Name = "Tanaka";
SingletonClass.Greeting(); // Hello, Tanaka
SingletonClass c = new SingletonClass(); // コンパイルエラー(別のインスタンスを生成できない)
}
virtualでメソッドのオーバライド
特にJava経験者の方は違和感を覚えるかと思いますが、子クラスで同じシグネチャのメソッドを用意しても、メソッドはオーバライドされません。そのため、c1.Greeting()では親クラスのメソッドが呼び出されます。
オーバライドするためには、親クラス側でオーバライドするメソッドにvirtualキーワードを付加する必要があります。子クラス側では、overrideキーワードを付与した(ただし無くてもコンパイルエラーにはならない)、同じシグネチャのメソッドを用意します。
子クラスでのvirtualメソッドのオーバライドは不要です。オーバライドしていない場合は、親クラスのメソッドが呼び出されます。
public class SuperClass
{
public void Greeting1()
{
System.Console.WriteLine("Hello!");
}
public virtual void Greeting2()
{
System.Console.WriteLine("Hello!");
}
}
public class SubClass : SuperClass
{
private string name;
public SubClass(string n)
{
name = n;
}
public void Greeting1()
{
System.Console.WriteLine("My name is " + name + "!");
}
public override void Greeting2()
{
System.Console.WriteLine("My name is " + name + "!");
}
}
public class TestClass
{
public static void Main()
{
SuperClass c1 = new SubClass("Araiwa");
c1.Greeting1(); // Hello!
((SubClass)c1).Greeting1(); // My name is Araiwa!
c1.Greeting2(); // My name is Araiwa!
((SubClass)c1).Greeting2(); // My name is Araiwa!
SuperClass c2 = new SuperClass();
c2.Greeting1(); // Hello!
c2.Greeting2(); // Hello!
SubClass c3 = new SubClass("Tanaka");
c3.Greeting1(); // My name is Tanaka!
c3.Greeting2(); // My name is Tanaka!
}
}
readonlyフィールド
コンストラクタ内であればreadonlyフィールドも初期化可能。
class X
{
public readonly int x;
public X(int i)
{
x = i;
}
}
public static void Main()
{
X c = new X(10);
System.Console.WriteLine(c.x); // 10
c.x = 100; // コンパイルエラー
}
匿名クラス
フィールドの名前・型・順番が一致していれば同じクラスとなる。
var item1 = new { Id = 1, Name = "豆腐", Price = 80 };
System.Console.WriteLine("{0}: {1}({2})", item1.Id, item1.Name, item1.Price); // 1: 豆腐(80)
item1.Name = "とうふ"; // コンパイルエラー(匿名クラスのフィールドはreadonlyとなるため更新できない)
int Id = 2, Price = 100;
string Name = "かまぼこ";
var item2 = new { Id, Name, Price }; // フィールド名の指定が無い場合、変数名がそのままフィールド名となる
System.Console.WriteLine(item1.GetType().Equals(item2.GetType())); // True(item1とitem2は同じクラス)
var item3 = new { Id = 3, Name = "お麩", Value = 150 };
System.Console.WriteLine(item1.GetType().Equals(item3.GetType())); // False
var item4 = new { Id = 4, Name = "納豆", Price = 90.0 };
System.Console.WriteLine(item1.GetType().Equals(item4.GetType())); // False
var item5 = new { Id = 5, Price = 70, Name = "こんにゃく" };
System.Console.WriteLine(item1.GetType().Equals(item5.GetType())); // False
プロパティ
自動実装プロパティ
NameとAgeはバッキングフィールド(自動実装されるプライベート変数)に値が格納される。
public class TestClass
{
private string id;
public string Id
{
get { return id; }
set { id = value.Length != 6 ? value : null; }
}
public string Name { get; set; }
public int Age { get; private set; }
}
プロパティのインターフェイスを定義・実装できる
public interface ITestClass
{
string Id { get; set; }
}
public class TestClass : ITestClass
{
string id;
public string Id
{
get { return id; }
set { id = value.Length == 6 ? value : null }
}
}
値型
構造体を定義できる
構造体は値型のデータ構造で、継承はできないが、インターフェイスは実装できる。
public struct Circle
{
public int X, Y, R;
public Circle(int x, int y, int r)
{
X = x;
Y = y;
R = r;
}
}
列挙型は先頭要素が0となる整数
途中で値を変更することもできる。
enum Number { Zero, One, Two, Ten=10, Eleven, Twelve };
// 0, 1, 2, 10, 11, 12が割り当てられる
デリゲート、イベント
マルチキャストデリゲート
+=でつなげることで、AddOne()、AddTwo()の順で実行される。-=で削除することもできる。
public class TestClass
{
static int x;
delegate int Add(); // デリゲートオブジェクト
static int AddOne()
{
return x += 1;
}
static int AddTwo()
{
return x += 2;
}
public static void Main()
{
x = 0;
Add add = AddOne;
add += AddTwo;
System.Console.WriteLine(add()); // 3
add -= AddOne;
System.Console.WriteLine(add()); // 5
}
}
マルチキャストイベント
elementAまたはelementBのプロパティが変更された時に、sumの値も再計算されるようにしている。
public class TestClass
{
delegate void ChangedEventHandler(int oldV, int newV);
class Element
{
public event ChangedEventHandler Handler;
int v;
public int V
{
get { return v; }
set
{
int oldV = v;
v = value;
if (Handler != null) Handler(oldV, v);
}
}
}
static void Main()
{
int sum = 0;
var elementA = new Element();
elementA.Handler += (oldV, newV) => sum = sum - oldV + newV;
var elementB = new Element();
elementB.Handler += (oldV, newV) => sum = sum - oldV + newV;
elementA.V = 1; // 1
System.Console.WriteLine(sum); // 1
elementB.V = 2;
System.Console.WriteLine(sum); // 3
}
}
ジェネリック
型パラメータ制約
親クラス制約・参照型制約(class)・値型制約(struct)のいずれか1つ、インタフェース制約、コンストラクタ制約(new())の順番で宣言する必要がある。
class Test1<T> where T : Super { } // クラスSuperを継承する型Tであること
class Test2<T> where T : IEnumerable { } // インタフェースIEnumerableを実装する型Tであること
class Test3<T, V> where T : class, new() where V : struct { } // 引数なしコンストラクタを持つ参照型Tで、かつ、値型Vであること
ジェネリック構造体
型パラメータの制約も可能です。
struct GenericElement<TKey, TValue>
{
public TKey key;
public TValue value;
public GenericElement (TKey k, TValue v)
{
key = k;
value = v;
}
}
ジェネリックメソッド
こちらも型パラメータ制約が可能。
public class TestClass
{
class GreetingClass
{
public void Greeting<T, V>(T name, V age, T message)
{
System.Console.WriteLine(name + "(" + age + "): " + message);
}
}
public static void Main()
{
var gc = new GreetingClass();
gc.Greeting("Araiwa", 40, "Hello!");
gc.Greeting<string, Byte>("Tanaka", 28, "Good Morning!"); // 型指定も可能
gc.Greeting("Umeda", 25, 'Y'); // 1番目と3番目の仮引数の型が一致しないためコンパイルエラー
}
}
名前空間
usingによる名前空間の指定
Linqを使うにはSystem.Linqを含める必要があるが、うっかり記述を忘れて、インテリセンスが効かずに「ん?」となることがよくある。
using System;
using System.Linq;
public class TestClass
{
public static void Main()
{
Console.WriteLine("Hello!"); // Systemを省略可
System.Console.WriteLine("World!"); // 省略しなくても良い
string[] prefectures = { "Tokyo", "Chiba", "Saitama", "Kanagawa", };
Console.WriteLine(prefectures.Where(p => p.EndsWith("a")).First()); // using System.LinqでLINQが使える
}
}
::演算子で名前空間エイリアスを指定する
global::とすることで、グローバル名前空間から指定できる。
(なお、namespaceで括られない場合はglobal名前空間に定義されたことになるので注意。)
using X = XNamespace;
namespace XNamespace
{
public static class XClass
{
public static void Greeting()
{
System.Console.WriteLine("XNamespace.XClass: Greeting()");
}
}
}
namespace X
{
public static class XClass
{
public static void Greeting()
{
System.Console.WriteLine("X.XClass: Greeting()");
}
}
}
public class TestClass
{
public static void Main()
{
X.XClass.Greeting(); // コンパイルエラー(エイリアス名Xと名前空間名Xが衝突するため)
X::XClass.Greeting(); // XNamespace.XClass: Greeting()
global::X.XClass.Greeting(); // X.XClass: Greeting()
}
}
名前空間とディレクトリ構成は無関係
作成したクラスは、デフォルトでプロジェクト名.フォルダ階層としてnamespaceが設定される。
しかし、別のフォルダに移したとしてもnamespaceまでは自動で変更されないので、開発者がnamespaceの記述を変える必要がある。
文献
独習C# 第3版(ハーバート・シルト 翔泳社)
Effective C# 4.0(ビル・ワグナー 翔泳社)
[完全版] 究極のC#プログラミング ~新スタイルによる実践的コーディング(川俣晶 技術評論社)
プログラミングC# 第7版(Ian Griffiths オライリージャパン)