[C#] C#を勉強したので文法などを一通りまとめた

C#言語の勉強メモ

byte型は unsigned

unsigned ではないものを使いたい場合は sbyte を利用する。

演算子のオーバーロード

  • public static メソッドとして定義する
  • 引数には必ず定義するクラスと同じクラスを含める。
  • 引数の数はオーバーロードする演算子によって固定。

    class D
    {
    public int Value { get; set; }
    public D(int value)
    {
    Value = value;
    }
    
    // 引数に別の型を含めることができるが
    // 1つは自身の型でなければならない
    public static D operator +(D y, int integer)
    {
    return new D(y.Value + integer);
    }
    }
    
    // 別の型で返すことも可能(+= が使えなくなる場合がある。そして無意味)
    public static int operator +(D arg1, D arg2)
    {
    return arg1.Value + arg2.Value;
    }
    
    static void Main(string[] args)
    {
    D d = new D(100);
    d += 10000;
    Debug.WriteLine(d.Value); // print "10100"
    
    int number = d + new D(1);
    Debug.WriteLine(number); // print "10101"
    }
    

explicit, implicit

型変換演算子のオーバーロードで使用する。implicitキーワードが付いている型は明示的なキャストが不要となる。

class X
{
  public static explicit operator int(X d)
  {
    return 123;
  }

  public static implicit operator string(X c)
  {
    return "foobar";
  }
}

static void Main(string[] args)
{
  X instance = new X();

  int number = (int)instance;
  string str = instance; // 暗黙的に変換される

  Debug.WriteLine(number); // print "123"
  Debug.WriteLine(str); // print "foobar"
}

enum

C言語のenumと同じではない。 Enum を継承した型を生成する糖衣構文になっている。 でも Enum を継承したクラスを書くとコンパイルできない。

enum Samples
{
  A,
  B,
  C,
  D = 100,
  E = -1,
}

static void Main(string[] args)
{
  Debug.WriteLine(Samples.A); // A
  Debug.WriteLine(Samples.B); // B
  Debug.WriteLine(Samples.C); // C

  Debug.WriteLine((int)Samples.A); // 0
  Debug.WriteLine((int)Samples.B); // 1
  Debug.WriteLine((int)Samples.C); // 2
  Debug.WriteLine((int)Samples.D); // 100
  Debug.WriteLine((int)Samples.E); // -1

  Debug.WriteLine(typeof(Samples).BaseType); // System.Enum
  Debug.WriteLine(Samples.A.GetType().BaseType); // System.Enum
}

int でキャストしないと数値として扱えない。

列挙に利用する型を変更する

デフォルトでは int 型だけど、byte, short, long, ulong などの整数型であれば変更することができる。

enum UnsignedShortEnum : ushort
{
    A = 99999 // コンパイルエラー
}

sealed キーワード

クラス宣言への修飾子で、sealed 修飾子の付いたクラスは継承できない。

また virtual メソッドのオーバーライド時に付けると以降の派生クラスでオーバーライドできなくなる。

base キーワード

Java でいう super キーワード。

protected, internal, protected internal

publicprivateJava と大差ないけれど、 protected は少し違うし internal というものもある。

  • internal: アセンブリ外からのアクセスを禁止する(特定の外部アセンブリから見えるように許可は可能→フレンドアセンブリ)。 外部アセンブリから見ると private だが、同じアセンブリ内からは public なので、同じアセンブリからも見えたら困る場合は private を使う。
  • protected: は継承するとアクセス可能になる修飾子(Java だと同じ protected メンバは同じパッケージ内ならアクセス可能だけど C# ではできない)
  • protected internal: 同じアセンブリ内 または サブクラス からアクセス可能。アンド条件ではないので注意。Javaprotected に似ている。

ちなみに C#C++friend 修飾子は無い。

var 型推論

ローカル変数の型を静的にコンパイラが推論して決定してくれる。

var d = new Dictionary<string, int>();

d["a"] = 10;
Debug.WriteLine(d["a"] == 10); // True

d[1] = 123; // コンパイルエラー
/*
 - 型推論により d は Dictionary<string, int> 型の変数となっているので、
 - キーに数値を指定することができない。
 */

匿名型

読み込み専用のプロパティを持ったオブジェクトを作成可能

var anonymous = new
{
  Foobar = 2000,
  Hoge = @"Hoge",
  Piyo = DateTime.Now,
};

int i = anonymous.Foobar;
string s = anonymous.Hoge;
DateTime d = anonymous.Piyo;

anonymous.Foobar = 123; // コンパイルエラー

dynamic

静的な型チェックを行わずに動的に型をチェックする型。 動的にオブジェクトにアクセス可能となる。 リフレクションの代わりに使用するとコードが簡潔になる。

class DynamicSample
{
    public int a = 0;

    public int A
    {
      get { return a; }
      set { a = value; }
    }

    public object Create()
    {
      return new DynamicSample();
    }
}

static void Main(string[] args)
{
    dynamic obj = new DynamicSample().Create();
    object buf = null;

    obj.a = 7;
    buf = obj.a;
    Debug.WriteLine(buf); // print "7"

    obj.A = 3;
    buf = obj.A;
    Debug.WriteLine(buf); // print "3"

    object other = obj.Create();
    Debug.WriteLine(other is DynamicSample); // print "True"
}
// 実際には動的にdllを読み込んだりするかと
var dll = System.Reflection.Assembly.LoadFrom("Hoge.dll");
dynamic obj = Activator.CreateInstance(dll.GetType("Hoge"));
dynamic obj = new DynamicSample().Create();

obj.a = 7;
try
{
  Debug.WriteLine(obj.a);
}
catch (Exception ex)
{
  Debug.WriteLine(ex);
  /*
   - Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 条件付き属性があるため、メソッド 'WriteLine' を動的に呼び出すことはできません
   - 場所 CallSite.Target(Closure , CallSite , Type , Object )
   - 場所 System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
   */
}

try
{
  obj.UndefinedMethod();
}
catch (Exception ex)
{
  Debug.WriteLine(ex);
  /*
   - 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException' の初回例外が 不明なモジュールです。 で発生しました。
   - Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'ConsoleApplication1.Program.DynamicSample' に 'UndefinedMethod' の定義がありません
   - 場所 CallSite.Target(Closure , CallSite , Object )
   - 場所 System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0](CallSite site, T0 arg0)
   */
}

foreach

int[] values = new int[] { 1, 2, 3 };
foreach (int value in values) {
  value;
}

for-in じゃなくて foreach-in

static クラス

static キーワードが付けられたクラスは…

  • static メンバしか持てない
  • インスタンス化できない
  • サブクラス化できない
  • Object以外を継承できない

拡張メソッド

public static class 好きな名前
{
  // 引数の this が拡張メソッドの印
  public static string Foobar(this int value)
  {
    return "foobar";
  }
}
class Program
{
  static void Main(string[] args)
  {
    Debug.WriteLine(100.Foobar()); // print "foobar"
  }
}

インスタンスメソッドっぽい見た目だけど実体はstaticメソッド

checked, unchecked

checked により整数型のオーバーフローのチェックを行うことができる。

unchecked はオーバーフローのチェックをしない。

浮動小数点型は checked しても無意味。

try
{
  int num = int.MaxValue;
  num = checked(num + 1);
}
catch (OverflowException ex)
{
  Console.Write("ここを通る");
}
try
{
  checked
  {
    int num = int.MaxValue;
    num++;
  }
}
catch (OverflowException ex)
{
  Console.Write("ここを通る");
}
try
{
  checked
  {
    int num = int.MaxValue;
    num = unchecked(num + 1);
  }
}
catch (OverflowException ex)
{
  Console.Write("ここは通らない");
}

null 合体演算子

三項演算子の亜種みたいな感じ。

string hoge;

hoge = piyo != null ? piyo : "foobar" ;
hoge = piyo ?? "foobar"; // 上の行と等価

null 許容型

C# ではプリミティブに見えるものも実はオブジェクトなんだけど、そういったものには null を代入できないが、 型名の後ろに ? をつけると null も代入可能となる。

Nullable<T> 型の糖衣構文

int? a = null;
int b = null; // コンパイルエラー

is 演算子

左辺の変数が右辺の型にキャスト可能なら真を返す演算子。Java で言えば instanceof 演算子に当たる。

object obj = 100;
bool isString = obj is string;
Debug.WriteLine(isString); // print "False"

as 演算子

通常キャストできない場合は例外が投げられるが、 as キーワードを使うとキャストできない場合に null を返す。

object obj = 100;
string str = obj as string;
Debug.WriteLine(str == null ? "NULL" : "NOT NULL"); // print "NULL"

プロパティ

private int value;
public int Value
{
  get { return value; }
  private set { this.value = value; } // 引数valueが自動生成される
}

こんな感じでプロパティを記述可能。 可視性を get,set で別にすることも可能。

public string Hoge { get; set; }
public string Piyo { set; }
public string Foober { get; } // 値がセットできないけど…

このような省略形も可能。 PascalCase のアクセサを定義してくれる。 プロパティはスタティック修飾子を使用することも可能

インデクサ

プロパティと似ているが微妙に違う点がある。

  • static なインデクサは定義できない
  • パラメータを複数指定できる

    class IndexerSample
    {
    private Dictionary<string, int> d = new Dictionary<string, int>();
    
    public int this[string key]
    {
    get { return d[key]; }
    set { d[key] = value; }
    }
    
    public int this[int a, int b] // 複数可能
    {
    get { return a + b; }
    }
    }
    
    static void Main(string[] args)
    {
    IndexerSample o = new IndexerSample();
    o["A"] = 10;
    
    Debug.WriteLine(o["A"]); // print "10"
    Debug.WriteLine(o[1, 2]); // print "3"
    }
    

オブジェクト初期化・コレクション初期化

class Hoge
{
  public string piyo;

  public static void Foobar()
  {
    Hoge hoge = new Hoge{ piyo = "HogePiyo"; }
    Console.Write("hoge.piyo");

    List<string> foo = new List<string>(){ "bar", "2000" };
    Dictionary<int, string> bar = new Dictionary<int, string>()
    {
      {1, "A"},
      {2, "B"},
      {3, "C"}
    };
  }
}

こんな感じで初期化可能。 コレクションクラスの初期化も同様の構文でできる。

readonly 就職し

フィールドにつけると、このフィールドは宣言時またはコンストラクタでしか値をセットできなくなる。

また、static readonlyconst のどちらでも定数定義が可能だが、 readonly は実行時にインスタンスを生成するが、 定数はコンパイル時にコードに埋め込まれるという違いがあり、 コンパイル時に解決が必要な部分に static readonly は使用できない。

using ステートメント と IDisposable

Java だと finally でリソース解放することが多いけど、 C# だと using ステートメント を利用するほうがスマート

using (StreamWriter writer = new StreamWriter("memo.txt", false, Encoding.UTF8))
{
  // do something...
}
// usingブロックを抜けるとリソースは解放される。
class Disposable : IDisposable
{
  public Disposable()
  {
    Debug.WriteLine("コンストラクタ");
  }

  public void Dispose()
  {
    Debug.WriteLine("Dispose");
  }

  /*
  public ~Disposable()
  {
    // ファイナライザで実行しておいたほうが良い
    Dispose();
  }
   */
}

static void Main(string[] args)
{
  using(Disposable disposable = new Disposable())
  {
    Debug.WriteLine("usingブロック内");
  }
  Debug.WriteLine("usingブロック外");
}

// 以下の順番で出力される

// コンストラクタ
// usingブロック内
// Dispose
// usingブロック外
  • IDisposable インターフェースを実装したインスタンスは必ず利用後に Dispose しなければならない。
  • IDisposable を実装しているクラスはたいていファイナライザで Dispose を実行している。
  • Dispose の実装は何度呼ばれても良いように書かれている…が例外は起こりうるので、Dispose が例外を投げる可能性がある場合は try ~ catch のほうがいいかも。
  • DisposeClose は別物。 Dispose 内で Close は呼ぶだろうけど別物。同じ挙動している可能性もあるけど別物。
  • アンマネージドリソースなどの解放を保証するための仕組み
  • using ステートメントは IDisposable を実装したクラスに対して利用可能
  • using ブロック無いから例外が投げられたり、goto で抜けたりしてもきちんと Dispose は実行される。
  • IDisposable#Disposeusing ブロックだけでなく foreach ブロックを抜けた時にも実行される

参考 : Dispose メソッドの実装

Disposable disposable = null;
using (disposable)
{
  disposable = new Disposable();
}
// これはDisposeが呼ばれない!

ref, out

ref 修飾子がついた引数は プリミティブ型でも参照渡しとなる。 参照型オブジェクトを out で渡した時、 out にもともと存在していたオブジェクトは消えるので要注意。

out に渡すためだけのローカル変数であっても、 初期値は設定しておくべきかも(out 引数に値が代入される前に例外が発生するとセットされない)

in, out, default

ジェネリクスで in は反変性, out は共変性を持つ総称型を定義可能。

defaultdefallt(string) だとか default(Type) みたいに使用して、 null 許容型ならば null を、null 非許容型ならばデフォルト値を返す。

Debug.WriteLine(default(string) ?? "null"); // null
Debug.WriteLine(default(int)); // 0
Debug.WriteLine(default(bool)); // False
Debug.WriteLine(default(decimal)); // 0
Debug.WriteLine(default(float)); // 0

列挙子 (IEnumerable, IEnumerator)

IEnumerable は配列やコレクションクラスが実装しているインターフェースで、 要素を列挙できるものに実装する。

IEnumerable を実装したクラスは foreach で全ての要素を取り出せる(※実際には IEnumerable を継承していなくても使える)。

IEnumerable<string> texts = new List<string> { "a", "b", "c" };
foreach(string text in texts)
{
  Debug.Write(text); // print "abc"
}

IEnumerable, IEnumerator の実装

IEnumerable<out T> インターフェースは

  • IEnumerator<T> GetEnumerator()
  • Enumerator GetEnumerator()

の2つを実装する必要がある。

IEnumerator<out T> インターフェースは IEnumerator, IDisposable を継承しているので、

  • T Current { get; }
  • object Current { get; }
  • bool MoveNext()
  • void Reset()
  • void Dispose()

の5つを実装する必要がある。

class ABCEnumerator<T> : IEnumerator<T>
{
  public T a;
  public T b;
  public T c;
  private int iterator = -1;

  T IEnumerator<T>.Current
  {
    get
    {
      Debug.WriteLine("Current");
      if (iterator == 0)
      {
        return a;
      }
      else
      {
        return iterator == 1 ? b : c;
      }
    }
  }

  public object Current
  {
    get
    {
      Debug.WriteLine("このコードではこっちは使われない");
      if (iterator == 0)
      {
        return a as object;
      }
      else
      {
        return iterator == 1 ? b : c;
      }
    }
  }

  public bool MoveNext()
  {
    iterator++;
    Debug.WriteLine("MoveNext(iterator = " + iterator + ")");
    return iterator < 3;
  }

  public void Reset()
  {
    Debug.WriteLine("Resetは勝手には呼ばれない");
    iterator = -1;
  }

  public void Dispose()
  {
    Debug.WriteLine("Dispose");
  }
}

class ABCEnumerable<T> : IEnumerable<T>
{
  private ABCEnumerator<T> enumerator;

  IEnumerator<T> IEnumerable<T>.GetEnumerator()
  {
    return enumerator;
  }

  // IEnumerator<out T> と IEnumerator は名前空間が違うので注意
  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return enumerator;
  }

  public ABCEnumerable(T a, T b, T c)
  {
    enumerator = new ABCEnumerator<T>();
    enumerator.a = a;
    enumerator.b = b;
    enumerator.c = c;
  }
}

static void Main(string[] args)
{
  foreach (string s in new ABCEnumerable<string>("A", "B", "C"))
  {
    Debug.WriteLine(s);
  }
}

結果

MoveNext(iterator = 0)
Current
A
MoveNext(iterator = 1)
Current
B
MoveNext(iterator = 2)
Current
C
MoveNext(iterator = 3)
Dispose

foreach ブロックを抜けるタイミングで Dispose が呼ばれている点に注意

参考 : IEnumerator<T> を実装していれば必ず IDisposable である理由 - NyaRuRuが地球にいたころ

上記リンクに IEnumerable を継承しなくても使えるって書いてあったのでやってみたらできた…

class Sample
{
  public IEnumerator<string> GetEnumerator()
  {
    var enumerator = new ABCEnumerator<string>();
    enumerator.a = "A";
    enumerator.b = "B";
    enumerator.c = "C";
    return enumerator;
  }
}

static void Main(string[] args)
{
  foreach (string s in new Sample())
  {
    Debug.WriteLine(s);
  }
}

イテレーターブロック

IEnumerator, IEnumerable を実装しようとすると最小限のコードでも気軽に実装できない。 でもイテレーターブロックを利用すると簡潔に IEnumerable, IEnumerator を返すメソッドを作成可能となる。 ただしイテレーターブロックは Reset メソッドを使えない。

public IEnumerable<char> AToZ()
{
  char aChar = 'A';
  while(aChar <= 'Z')
  {
    yield return aCher++;
  }
}

public test()
{
  for(char aCher in AToZ())
  {
    // do something
  }
}

yield return で返すのがポイント。また break ではなく yield break を使う。 戻り値は Enumrable, Enumerable<T>, IEnumerator, IEnumerator<T> だけが使える。 動きとしては yield return でイテレーターブロックは抜けて、 もう一度呼ばれた時に yield return で返されたところからまた始まる。 なので、 yield return の後の行も読まれる可能性がある(もちろん読まれない可能性も)

プリプロセッサ ディレクティブ

C# には C/C++ のような独立したプリプロセッサマクロは存在しないが、プロプロセッサ命令は用意されている。C/C++ のような使い方をしようとするとエラー出まくりなので注意。C# でのプリプロセッサ命令は単純な条件分岐コンパイルくらいにしか利用できない。

  • #define, #undef はファイルの最初以外には書けない
  • #defineC/C++ のように自由に定義することはできない。
  • #define は シンボル名 を定義できるのみ。値は不可!
  • #ifdef は存在しない
  • リリース時には #define されたシンボルはデフォルトでは存在しない
  • #if, #elif での判定に !&&|| を利用できる
  • #if, #elif での判定に ==!= を使えるけど使いドコロがほぼ無い…
  • #if DEBUG#if DEBUG != null と等価。 null を使えるけど使う意味が無い…
  • #warning, #error でプリプロセス時に 警告・エラーを発生させることが可能
  • #line はデバッグ時のステップに影響があったり、行数を変えれたり、hidden で隠したり

#region リージョン名 ~ #endregion

#region ~ #endregion で囲まれた部分は折りたたみ可能となる。

でも以下の様なやり方をすると混乱するかもしれないので注意。

  class Region
  {
#region このような使い方も可能なので注意する事
  }
#endregion

例外処理

try
{
  // do something...
}
catch (Exception ex)
{
  Console.WriteLine(ex);
}

例外の再スロー と throws

例外を再スローする場合は…
throw ex; ではなく throw; とすること。

throw ex; で例外スローするとその行からのスタックトレースが例外にセットされる。

Java には throws があるけど C# には無い (Java の視点で言えばすべて非チェック例外しかない)

switchcase に文字列を使用可能

switchJava と違って文字列も使える。 ただしコンパイル時に解決できない場合は不可

virtual

C++ と同じく仮想メソッドを使用するにはこの修飾子をつける必要がある。

配列

Java と同様に四角カッコの配列は共変性であり、 必要がなければ利用すべきではない(型安全ではないため)。List を使うべき。

string[] array = { "foo", "bar" };
array = new string[] { "foo", "bar" };
array = { "foo", "bar" }; // コンパイルエラー
object[] array = { new object(), new object() };
object[] copy = array.ToArray(); // コピー

名前付き引数

static void Method(int i, float f)
{
  Debug.WriteLine("A");
}

static void Method(float ff, int ii)
{
  Debug.WriteLine("B");
}

static void Main(string[] args)
{
  Method(i: 1, f: 0.1f); // A
  Method(f: 0.1f, i: 1); // A
  Method(ii: 1, ff: 0.1f); // B
  Method(ff: 0.1f, ii: 1); // B
}
static void Method(int a = 10, int b = 11, int c = 12)
{
  Debug.WriteLine("a={0}, b={1}, c={2}", a, b, c);
}

static void Main(string[] args)
{
  Method(); // a=10, b=11, c=12
  Method(c: 999); // a=10, b=11, c=999
}

可変長引数 (params)

params 型[] 引数名 で可変長引数となる。

static void Method(params int[] args)
{
  foreach(int arg in args)
  {
Debug.Write(arg + ", ");
  }
}

static void Main(string[] args)
{
  Method(1, 2, 3, 4, 5);
}

コンストラクタ初期化子

C++ のとは違ってコンストラクタに対してのみ利用可能

 {
    public int property { get; set; }
    public Value() { Debug.WriteLine("Value#Constructor"); }
}

class Sample
{
    public Value Value = new Value();

    public Sample() : this(123)
    {
        Debug.WriteLine("Sample#Constructor()");
    }

    public Sample(int arg)
    {
        Debug.WriteLine("Sample#Constructor(int arg)");
        Value.property = arg;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Debug.WriteLine(new Sample().Value.property);
    }
}

結果

Value#Constructor
Sample#Constructor(int arg)
Sample#Constructor()
123

変数初期化子 → コンストラクター初期化子 → コンストラクター本体 の順番で実行される。

構造体

構造体のクラスとの大きな違いはヒープではなくスタックに置かれること。

  • データの構造化 (C# によるプログラミング入門)
  • 構造体の使用 (C#)

  • メンバ変数をフィールド、メンバ関数をメソッドと呼ぶ

  • 構造体のコンストラクタは定義できない(メンバ変数を規定値に初期化するコンストラクタをコンパイラが作る)

  • 定数や static メンバは変数初期化子を利用できる

  • 基本的には new でインスタンス化する。でもnewなしでもインスタンス化可能(初期化が必要)

  • 継承ができない (System.ValueType を継承する)

  • 参照型ではなく値型

  • インターフェースの実装ができる

初期化しなかったメンバ変数の初期値

null 許容型は null, で null 非許容型は 0 に相当する値で初期化される。 ローカル変数は初期化されず、コンパイラがエラーを吐く。

  public class Sample
  {
    public static bool boolean;
    public static int integer;
    public static string str;
    public static DateTime time;
    public static DateTime? timeAllowedNull;

    public static DateTime TimeAccessor { get; set; }

    public static void Main()
    {
      Debug.WriteLine(boolean); // False
      Debug.WriteLine(integer); // 0
      Debug.WriteLine(str ?? "null"); // null
      Debug.WriteLine(time); // 0001/01/01 0:00:00
      Debug.WriteLine(timeAllowedNull == null); // True
      Debug.WriteLine(TimeAccessor); // 0001/01/01 0:00:00

      string hoge;
      Debug.WriteLine(hoge); // コンパイルエラー
    }
  }
Share
関連記事