C# 7 早わかりリファレンス(中編)

C# 7 早わかりリファレンス(中編)

C# クラスの基本機能 最速再入門【7.0対応】

2017年4月27日 改訂(初版[6.0対応]: 2016/04/14)

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。中編では、名前空間/クラス/メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクターを説明する。

  • このエントリーをはてなブックマークに追加

 前編ではC#言語の文法の中でも最もベーシックな「型」「変数」「演算子」「ステートメント(文)」の基本機能をできるだけコンパクトに説明した。今回中編では、オブジェクト指向言語であるC#の要となる「クラス」関連の機能として「名前空間」「クラス(メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクター)」について説明する。

<目次>【前編】【中編】【後編】のラベルをクリック/タップすると開閉します。)

▲目次に戻る

名前空間

▲目次に戻る

名前空間宣言完全修飾子

C#
class Program
{
  static void Main(string[] args)
  {
    var a = new N1.A();
    var b = new N1.N2.B();
    var c = new N1.N2.C();
  }
}

namespace N1
{
  class A { }

  namespace N2
  {
    class C { }
  }
}

namespace N1.N2
{
  class B { }
}
リスト5-1 名前空間宣言と完全修飾子

 C#プログラムは名前空間を使ってクラスの分類・階層を整理している。.NET Framework自身が多数のクラスを編成するために名前空間を利用しており、自分で開発する際も名前空間を利用してクラスを整理できる。

 名前空間は階層的に定義でき、名前空間を宣言する際はnamespaceキーワードを使う。階層的な名前空間は、複数のnamespaceを入れ子にして(例:リスト5-1のN1N2)、もしくは名前空間を.でつなげて(例:リスト5-1のN1.N2)宣言できる。

 別の名前空間内に定義されたクラスを参照する場合、名前空間とクラス名を.でつなげた完全修飾子名で指定する。もしくは次の項で紹介するusingディレクティブを利用する。

 なお、名前空間の命名規則であるが、Microsoftのガイドラインでは企業名.製品名.機能名といった安定した名前を利用し、組織構造に変更があったときに名前空間に影響があるような名前付けは避けるべきとしている。

▲目次に戻る

usingディレクティブ

C#
using System;
using System.Text;

namespace CSharpCheatSheet
{
  using MyDateTime = N2.DateTime;

  class Program
  {
    static void Main(string[] args)
    {
      // usingによりSystem.Text.StringBuilderを参照
      var sb = new StringBuilder();
      // System.DateTimeを参照
      var now = DateTime.Now;
      // namespace aliasによりCSharpCheatSheet.N2.MyDateTimeを参照
      var dateTime = new MyDateTime();
      // namespace aliasがない場合はこのように全ての参照箇所で完全修飾子が必要になる
      var dateTime2 = new N2.DateTime();
    }
  }
}

namespace N2
{
  public class DateTime { }
}
リスト5-2 usingディレクティブとusing aliasディレクティブ

 usingディレクティブを利用すると、完全修飾子を使わずにクラス名のみで別の名前空間に属するクラスを参照できるようになる。

 using aliasディレクティブを使うと、名前空間またはクラス名のエイリアス(別名)を定義できる。例えばリスト5-2はMyDateTimeをクラス名「DateTime」のエイリアスとして利用しているが、このDateTimeのように.NET Frameworkの基本クラスライブラリに含まれるクラスと同じ名前のクラスが定義されている別のライブラリを利用する場合、通常であれば、DateTimeへの参照が出現する箇所全てに完全修飾子を使って、どちらのDateTimeなのかを明確に記述する必要がある。エイリアスを使えば、その別名を使って明確に参照できるので、完全修飾子にする必要はなくなる(前項にも出したガイドラインによれば、.NET Frameworkのクラス名と同じ名前は付けるべきではないが、参照するライブラリがそのガイドラインにのっとっていない場合があるため。また、複数のクラスライブラリを使うと、偶然、名前が同じクラスが定義されている場合もあるが、そんな場合にusing aliasが便利だ)。

▲目次に戻る

クラス

▲目次に戻る

クラスの宣言

C#
using System;

namespace Library
{
  public class MyClass
  {
    // フィールド
    public static readonly int DefaultLimit = 10;

    private int term;

    // コンストラクター
    public MyClass() : this(DefaultLimit)
    { }

    public MyClass(int limit)
    {
      Limit = limit;
    }

    // プロパティ
    public int Limit { get; }

    // メソッド
    public void Try()
    {
      term = 0;
      while (term < Limit)
      {
        Console.WriteLine($"{term}回目");
        ++term;
      }
    }
  }
}
リスト6-1 クラスの宣言

 クラスは、フィールド、メソッド、コンストラクター、プロパティ、インデクサーなどを格納するデータ構造である。C#ではトップレベルのクラスはソースコードファイル(=.csファイル)の中に複数定義できる。

▲目次に戻る

静的クラス

C#
class Program
{
  static void Main(string[] args)
  {
    var mile = MileConverter.MileToKm(1);
  }
}

// 静的クラス
public static class MileConverter
{
  // 静的メンバー(フィールド)
  public static readonly double MilePerKm = 0.62137;

  // 静的メンバー(メソッド)
  public static double KmToMile(double km)
  {
    return km * MilePerKm;
  }

  public static double MileToKm(double mile)
  {
    return mile / MilePerKm;
  }
}
リスト6-2 静的クラス

 静的(static)クラスは、インスタンス化できないクラスで、静的メンバーのみを含めることができる。後述する拡張メソッドを定義するため利用されることも多い。

▲目次に戻る

部分クラス

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.Test();
    my.Test2();
  }
}

// 部分クラス(1つ目)
public partial class MyClass
{
  public void Test()
  {
    Console.WriteLine("Test");
  }
}

// 部分クラス(2つ目)
public partial class MyClass
{
  public void Test2()
  {
    Console.WriteLine("Test2");
  }
}
リスト6-3 部分クラス

 部分(partial)クラスは、1つのクラス定義を複数個所に分けて記述できる仕組みである。例えば、クラスの定義の一部を自動生成する場合などに、自動生成部分をpartialで分離することにより、自動生成を再実行しやすくするといった使い方をする。

▲目次に戻る

ジェネリクスの型パラメーター

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var c1 = new MyClass<string>();
    c1.Value = "a";
    c1.DefaultValue();

    var c2 = new MyClass<DateTime>();
    c2.Value = DateTime.Now;
    c2.DefaultValue();

    var c3 = new MyClass2<object>();
    c3.DefaultValue();

    var c4 = new MyClass3<Printable>();
    c4.Execute();

    var c5 = new MyClass4<string, DateTime>();
  }
}

// ジェネリック(=汎用的な)クラス。Tが型パラメーター
public class MyClass<T>
{
  public T Value { get; set; }

  public T DefaultValue()
  {
    return default(T);
  }
}

public class MyClass2<T> where T : new()
{
  public T DefaultValue()
  {
    // new () 制約があるため、Tのデフォルトコンストラクター呼び出しによるインスタンス化が可能
    return new T();
  }
}

public class MyClass3<T> where T : IPrintable, new()
{
  public void Execute()
  {
    // TはIPrintableインターフェースを実装している
    new T().Print();
  }
}

public interface IPrintable
{
  void Print();
}

public class Printable : IPrintable
{
  public void Print()
  {
    Console.WriteLine("Printable Prints");
  }
}

// 複数の型パラメーターがある場合は、それぞれに制約を指定可能
public class MyClass4<K, V>
  where K : class
  where V : struct
{

}
リスト6-4 型パラメーター

 型パラメーターを利用することで、クラスを定義する際にプレースホルダー(上記コードではTKV)として型を提供し、そのクラスを利用する際に実際の型(上記コードではstringDateTimeなど)を指定できる。ジェネリック(=汎用的)に使えるクラス/インターフェース/メソッドなどを定義できるので、ジェネリクス(Generics)と呼ばれ、基本クラスライブラリでもList<T>クラスなど主にコレクションで多用されている。

 型パラメーターはwhereキーワードで制約を課すことができる。classもしくはstructで「参照型」もしくは「構造体」である制約を課したり、「指定したインターフェースを実装したクラス」や「指定したクラスを継承したクラス」という制約を課したりできる。また、new ()制約により「publicな引数なしのコンストラクターを持つ」という制約を課すことで、型パラメーターに指定したクラスを(例えばnew T()という)コンストラクター呼び出しでインスタンス化することもできる。

▲目次に戻る

アクセス修飾子入れ子クラス

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    // protected internalなOuter1.Inner3クラスにはアクセスできるが、
    // privateなOuter1.Inner2クラスにはアクセスできない
    var inner = new Outer1.Inner3();
    inner.Execute();

    var outer = new Outer1();
    outer.Execute();
  }
}

public class Outer1
{
  private static void Run()
  {
    Console.WriteLine("Run");
  }

  public void Execute()
  {
    // 包含する型からなのでprivateなInner2にアクセスできる
    var inner = new Inner2();
    inner.Execute();
  }

  public class Inner1
  {
    public void Execute()
    {
      // 入れ子になったクラスから包含する型のprivateメソッドにアクセスできる
      Run();
    }
  }

  private class Inner2 : Inner1
  { }

  protected internal class Inner3 : Inner1
  { }
}
リスト6-5 入れ子になったクラスとアクセス修飾子

 トップレベルのクラスはpublicもしくはinternalにできる。省略した場合のデフォルトはinternalになる。internalは同じアセンブリ内からのみアクセス可能だ。

 また、クラスの内部に入れ子になったクラスを定義でき、この場合、privateprotectedinternalprotected internalpublicにできる。privateはそのクラス自身からのみ、protectedはそのクラス自身および派生したクラスからのみアクセス可能であり、protected internalprotectedもしくはinternalなアクセスが可能である。省略した場合のデフォルトはクラスのフィールドやプロパティ同様、privateである。

 入れ子になった型とそれを包含する型は、インスタンス同士に特別な関係はない。つまり包含する型のインスタンスと無関係に入れ子になった型をインスタンス化できる。入れ子になった型は、包含する型のprivateprotectedなメンバーにアクセス可能である。

▲目次に戻る

定数static readonlyフィールド

C#
// Library.dll
using System;

namespace Library
{
  public class Utility
  {
    public const int X = 1;              // 定数
    public static readonly int Y = 1;    // 静的な読み取り専用
    // コンパイル時に値が決まらない場合はstatic readonlyフィールドが利用できる
    public static readonly DateTime DefaultDate = new DateTime(2016, 4, 1);
  }
}

// ConsoleApp.exe
using System;
using Library;

namespace CSharpCheatSheet
{

  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine(Utility.X);
      Console.WriteLine(Utility.Y);
    }
  }
}
リスト6-6 定数とstatic readonlyフィールド

このサンプルに対応するLINQPad用サンプルは存在しない。試すには、コンソールアプリ(ConsoleApp)とクラスライブラリ(Library)のプロジェクトを作成し、アプリ側からライブラリを参照してほしい。

 定数は、コンパイル時に値が決定するものをクラスメンバーとして宣言できる仕組みである。定数は静的にアクセスできるが、static修飾子は不要でstatic constと記述するとコンパイルエラーになる。

 定数とよく似た働きをするものとして、static readonlyなフィールドがある。コンパイル時に決定できない値、例えば引数を指定したDateTimeクラスのインスタンスなどはstatic readonlyなフィールドを利用できる。

 定数の利用に注意しないといけない場合として、アセンブリをまたがったときに参照されている側のアセンブリで定数を更新した場合の扱いがある。サンプルコードのように、Library.dll側に定数とstatic readonlyなフィールドを定義し、ConsoleApp.exeはLibrary.dllを参照しているものとしよう。このとき、Library.dll側の定数(X)とstatic readonlyなフィールドの値(Y)をともに1から2に変更した新しいバージョンのLibrary.dllに更新する状況を考える。コンパイルし直した新しいLibrary.dllを参照しているConsoleApp.exeのソースコードをコンパイルし直す場合は問題なくXY両方の値が2と表示される。しかし、ConsoleApp.exeをコンパイルし直すことなく、Library.dllのみを新しいものに更新した場合、定数の値は1と表示されたままである。これは、定数の値はConsoelApp.exeのコンパイル時のLibrary.dllの値を参照したままになるためだ。

 このような性質があるため、プログラムを更新する際に変更する可能性がある値は定数として宣言するのを避けた方がいいだろう。

▲目次に戻る

メソッド

▲目次に戻る

メソッドの宣言アクセス修飾子

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.Run("text");
    MyClass.InternalRun();
  }
}

public class MyClass
{

  static internal void InternalRun()
  {
    Console.WriteLine("static internal Run");
  }

  public void Run(string text)
  {
    Console.WriteLine(text);
  }

  protected void ProtectedRun()
  {
    Console.WriteLine("ProtectedRun");
  }

  private void PrivateRun()
  {
    Console.WriteLine("InternaRun");
  }
}
リスト7-1 メソッドの宣言とアクセス修飾子

 クラスにはメソッドを含めることができる。メソッドはパラメーター(引数)リストを宣言できる。メソッドのアクセス修飾子は入れ子になった型と同じく、publicinternalprotectedprotected internalprivateを指定できる。またstaticなメソッドは静的メソッドと呼ばれ、その型のインスタンスではなく、クラス型に関連してメソッドを呼び出せる。

▲目次に戻る

refパラメーターoutパラメーター

C#
class Program
{
  static void Main(string[] args)
  {
    var i = 1;
    var j = 2;
    Swap(ref i, ref j);
    // i=2
    // j=1
    var k = 0;
    int l;
    // 初期化済みの値、未初期化の値両方利用できる
    Out(out k, out l);
    // k=1
    // l=2
  }

  static void Swap(ref int x, ref int y)
  {
    var temp = x;
    x = y;
    y = temp;
  }

  static void Out(out int x, out int y)
  {
    x = 1;
    y = 2;
  }
}
リスト7-2 refパラメーターとoutパラメーター

 修飾子のないメソッドの引数は値パラメーターであり、メソッド内でパラメーターの値を代入してもメソッドを呼び出す側の変数には影響を与えない。

 しかし、refキーワードを指定して参照パラメーターとして宣言すると、メソッドを呼び出す側とメソッド内で同じ変数を参照することになる。つまりメソッド内でパラメーターを代入し直すと、その変更が呼び出し側の変数にも影響する。

 outキーワードは出力パラメーターとして宣言するときに使われ、参照パラメーターと同じような効果がある。出力パラメーターは複数の値を返す場合に利用され、メソッドを呼び出す時点で変数を初期化しておく必要がない点とメソッド内で必ず値を代入しないといけない点が異なる。

 また、C# 7.0よりoutパラメーターで変数宣言できるようになった(リスト7-2b)。

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    if (Out(out var x1, out var y1))
      Console.WriteLine($"x={x1}, y={y1}"); // x=1, y=2
    // x1とy1のスコープはif文内だけでなくif文が記載されているスコープ内
    Console.WriteLine($"x={x1}, y={y1}");   // x=1, y=2
    
    // _で使わない変数を読み捨てることができる
    if (Out(out int x2, out var _))
      Console.WriteLine($"x={x2}");         // x=1
    // 読み捨てに利用する識別子_は明示的に宣言した変数の識別子_とは別物
    var _ = 1;
    Console.WriteLine($"_={_}");            // _=1
  }

  static bool Out(out int x, out int y)
  {
    x = 1;
    y = 2;
    return true;
  }
}
リスト7-2b Out Var

 このコードを見ると分かるように、outパラメーターを指定してのメソッド呼び出しの際に、outキーワードと同時に変数宣言ができるようになったことにより、事前に変数宣言をする必要がなくなった。変数宣言は明示的に型を指定する方法と、varキーワードを使い暗黙的に型を指定する方法の両方が使える。また、変数のスコープは例えばif文の条件判定で利用すると、if文内だけでなくif文が記載されているスコープの中になるが、当然、変数宣言より前に参照することはできない。また、_を使うことで利用しない変数を読み捨てることができるが、これは明示的に識別子_を使って宣言した変数とは別物の扱いとなる。

 なお、C# 7.0でrefキーワードをメソッドやローカル変数に付ける参照返り値参照ローカル変数が導入された。これは後述の別稿を参照してほしい。

▲目次に戻る

パラメーター配列

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var array = new[] { 1, 2, 3 };
    // 3個の引数が指定されました
    Param(array);
    // 2個の引数が指定されました
    Param(1, 2);
    // 0個の引数が指定されました
    Param();
  }

  static void Param(params int[] array)
  {
    Console.WriteLine($"{array.Length}個の引数が指定されました");
  }
}
リスト7-3 パラメーター配列

 paramsキーワードを指定することでパラメーター配列を定義し、可変長な引数を扱うことができる。パラメーター配列には、配列そのものの他、配列の要素型の値を,で区切ったパラメーターリストを指定できる。メソッド内ではパラメーター配列は配列として扱うことができ、パラメーター配列を指定せずに呼び出した場合は長さ0の配列が代入されている。

▲目次に戻る

省略可能なパラメーター

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    // x=1 y=a
    Test(1);
    // x=2 y=b
    Test(2, "b");
  }

  static void Test(int x, string y = "a")
  {
    Console.WriteLine($"x={x} y={y}");
  }
}
リスト7-4 省略可能なパラメーター

 メソッドのパラメーターは、デフォルト値を指定することで省略可能なパラメーターにできる。デフォルト値は、コンパイル時に決定できる定数でなければならない。また、省略可能なパラメーターの後に、省略できない必須のパラメーターを宣言することはできない。

▲目次に戻る

拡張メソッド

C#
class Program
{
  static void Main(string[] args)
  {
    var s = "abcd";
    var text = "CD";
    // true
    var flg = s.ContainsIgnoreCase(text);
  }
}
public static class MyExtensions
{
  public static bool ContainsIgnoreCase(this string s, string text)
  {
    return s.ToLower().Contains(text.ToLower());
  }
}
リスト7-5 拡張メソッド

 拡張メソッドを定義することにより、クラス定義を変更することなく、あたかもそのクラスにインスタンスメソッドを追加したかのように、メソッドを呼び出すことができる。静的クラスに宣言した静的メソッドの最初のパラメーターにthisキーワードを付けることで定義する。拡張メソッドを定義した静的クラスが別の名前空間に属する場合は、その名前空間をusingすることで利用可能になる。LINQLanguage INtegrated Query)のメソッド形式の多くは、拡張メソッドにより定義されており、using System.Linqにより参照可能になる。

▲目次に戻る

参照返り値参照ローカル変数

 C#では返り値が値型の場合、代入時に値の複製が発生する。そのため、データ構造的に大きい値型を返すことはパフォーマンス上、懸念される場面がある。このような場面に対処するために、C# 6.0以前ではrefパラメーターによる引数の参照渡しはできていたが、C# 7.0ではさらに返り値とローカル変数の参照渡しも可能になった。

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    int[] array = { 1, 15, -39, 0, 7, 14, -12 };
    ref int place = ref Find(7, array);  // 値が7の配列要素の参照を取得
    place = 9;                           // 参照先の配列要素の値を書き換える
    Console.WriteLine(array[4]);         // 5番目の要素の値が「7」から「9」に書き換わっている

    var a = 100;
    ref int b = ref a;  // bとaの参照は同じ
    var c = b;          // cにbのその時の値「100」を代入
    ref var d = ref b;  // dとbの参照は同じ。aとも同じ参照
    ++b;   // bに1を足すので、a/b/dとも101
    ++a;   // さらにaに1を足すので、a/b /dとも102
    ++c;   // cに1を足して、cだけ101
    Console.WriteLine($"{a},{b},{c},{d}"); // 102,102,101,102
  }

  static ref int Find(int number, int[] numbers)
  {
    for (int i = 0; i < numbers.Length; i++)
    {
      if (numbers[i] == number)
      {
        return ref numbers[i];  // 配列の値ではなく参照を返す
      }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
  }
}
リスト7-6 参照返り値と参照ローカル変数

 このサンプルコードを見て分かるように、メソッドの返り値の型、メソッド内でのreturn、メソッド呼び出し、参照渡しの返り値を受け取る変数宣言の全てに、refキーワードを付ける必要がある。このことには「参照渡しという機能は副作用が大きいため、その副作用を認識しやすくする」という意味も含められている。また任意の変数を参照渡しできるわけではなく、ローカル変数や引数で与えられた配列の要素など、参照を渡しても安全なものに限り参照渡しができるようになっている。

▲目次に戻る

プロパティ

▲目次に戻る

プロパティの宣言

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.Value1 = 3;
  }
}

public class MyClass
{
  private int value1;
  public int Value1
  {
    get
    {
      return value1;
    }
    set
    {
      if (value != value1)
      {
        value1 = value;
        ValueChanged();
      }
    }
  }

  private void ValueChanged()
  {
    Console.WriteLine("ValueChanged");
  }

  private int value2;
  // C# 7.0より式形式で記述可能
  public int Value2
  {
    get => value2;                 // getterはC# 6.0より可能
    set => value2 = value;         // setter
  }
}
リスト8-1 プロパティ

 プロパティは、クラスの構成要素の一つでフィールドを拡張したものだ。フィールドのようにアクセスできるが、gettersetterとして処理を定義できる。setter内では、プロパティに代入された値をvalueキーワードで参照できる。また、C# 6.0からはgetterが、C# 7.0からはsetterも式形式で記述できるようになった。

▲目次に戻る

自動実装プロパティ

C#
class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.UpdateValue1();
    my.Value2 = 12;
  }
}
public class MyClass
{
  public int Value1 { get; private set; }

  public int Value2 { get; set; } = 2;  // 自動実装プロパティの初期化

  public int Value3 { get; }

  public MyClass()
  {
    // getter onlyプロパティはコンストラクターでも代入可能
    Value3 = 3;
  }

  public void UpdateValue1()
  {
    // privateなsetterなのでインスタンス内から変更可能
    // Value3は更新できない
    Value1 = 1;
  }
}
リスト8-2 自動実装プロパティ

 自動実装プロパティは、単純にフィールドの値を読み書きするだけのプロパティの記述を簡単に記述できるようにしたものである。C# 6.0で自動実装プロパティの初期化がフィールドと同じような形式でできるようになったのに加え、getterのみの自動実装プロパティが記述できるようになったため、変更不可能なプロパティが記述しやすくなった。C# 6.0より前から定義できたprivateなsetterを持つ自動実装プロパティは、同じクラスの別のメソッドなどから変更可能であることに注意してほしい。

▲目次に戻る

イベント

▲目次に戻る

イベントの定義呼び出し

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    new Program().Execute();
  }

  void Execute()
  {
    // EventHandlerのインスタンスを追加する、もしくは直接追加可能。
    // 意味合いとしては同じだが、別のインスタンスとして追加される
    Changed += new EventHandler(OnChanged);
    Changed += OnChanged;
    // EventHandlerに1つも購読のハンドラーが登録されていないとnullであるため、
    // null条件演算子経由でイベントを発火させている
    Changed?.Invoke(this, new EventArgs());
    // イベントの購読を解除できる
    Changed -= OnChanged;
    Changed?.Invoke(this, new EventArgs());

    ValueChanged += OnValueChanged;
    ValueChanged.Invoke(this, new MyEventArgs(3));
  }

  void OnChanged(object sender, EventArgs ea)
  {
    Console.WriteLine("OnChanged");
  }

  void OnValueChanged(object sender, MyEventArgs ea)
  {
    Console.WriteLine($"OnValueChanged {ea.Value}");
  }

  event EventHandler Changed;
  event EventHandler<MyEventArgs> ValueChanged;

  class MyEventArgs : EventArgs
  {
    public int Value { get; }
    public MyEventArgs(int value)
    {
      Value = value;
    }
  }

  // event構文はaddとremove処理を明示的に記述できる。
  // 省略した場合のコードは意味合い的には下記のコードと同様だが、
  // 実際にはスレッドセーフなコードが生成されている
  private EventHandler customHandler;
  event EventHandler CustomHandler
  {
    add
    {
      customHandler += value;
    }
    remove
    {
      customHandler -= value;
    }
  }

  // 自前で定義したdelegateをイベントの型として利用することもできる
  public delegate void MyEventHandler();
  public event MyEventHandler MyChanged;

  private int counter = 0;
  // C# 7.0より式形式で記述可能
  public event Action E
  {
    add => ++counter;
    remove => --counter;
  }
}
リスト9-1 イベント

 イベントは、オブジェクトが通知を発行・購読できるようにした仕組みである。通知を発行したい側はeventキーワードによりイベントを公開する。通常、EventHandlerデリゲートdelegate)もしくはそのジェネリクス型のEventHandler<TEventArgs>デリゲート(いずれもSystem名前空間)を利用するが、自前で定義したdelegate型を利用することもできる。イベント引数の型はEventArgsを継承して自作することが一般的だ。

 購読する側はdelegateと同じ型のメソッドを登録する。後述するラムダ式を利用することもできる。購読は+=演算子、解除は-=演算子で行うが、購読したインスタンスと同一のインスタンスを指定しない限り解除はできない。また、通知を送信する側が購読側を解除する仕組みはない。

 通知する側はイベントハンドラーを呼び出すことでイベントを通知できる。イベントハンドラーにイベントが登録されていないとnull呼び出しで実行時エラーになるため、null条件演算子を利用するなどしてnullチェックを行う必要がある。

 また、イベントハンドラーはイベントを追加・削除する時の処理を明示的に記述できる。しかしスレッドセーフに実装するべき箇所であるため、明示的に記述する際はその点を考慮するのがいいだろう。

 C# 7.0からは、イベントハンドラーへの追加と削除の処理を式形式で記述できるようになった。

▲目次に戻る

インデクサー

▲目次に戻る

インデクサーの定義

C#
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my[1] = 100;
    var v1 = my[1];
    var v2 = my["test"];
  }
}


public class MyClass
{
  private int[] array = new int[100];
  private Dictionary<string, int> dict = new Dictionary<string, int>();

  // int型のインデクサー
  public int this[int i]
  {
    get
    {
      return array[i];
    }
    set
    {
      array[i] = value;
    }
  }

  // getterのみの場合は、式形式で記述可能
  public int this[string s] => dict.ContainsKey(s) ? dict[s] : 0;
}

public class Ex10_1_Indexer_MyClass2
{
  private Dictionary<string, int> dict = new Dictionary<string, int>();
  // C# 7.0より式形式で記述可能
  public int this[string s]
  {
    get => dict.ContainsKey(s) ? dict[s] : 0;
    set => dict[s] = value;
  }
}
リスト10-1 インデクサー

 インデクサーは、配列アクセスのようにインデックスを使用してオブジェクト内部のコレクション要素などにアクセスするための仕組みである。インデックスにはint型や文字列型を使うことが多いが、それ以外の型も利用可能であり、複数の型の組み合わせを指定することもできる。プロパティ同様、getterとsetterを記述する。C# 6.0ではgetterが、C# 7.0以降はsetterも、式形式の記述をすることが可能になった。

▲目次に戻る

演算子オーバーロード

▲目次に戻る

演算子オーバーロードの定義

C#
class Program
{
  static void Main(string[] args)
  {
    var c1 = new Complex(1, 3) + new Complex(2, 4); // 3 + 7i

    var c2 = -new Complex(2, -2); // -2 + 2i
  }
}

public class Complex
{
  public double Real { get; }
  public double Imaginary { get; }

  public Complex(double real, double imaginary)
  {
    Real = real;
    Imaginary = imaginary;
  }

  public static Complex operator -(Complex c1)
  {
    return new Complex(-c1.Real, -c1.Imaginary);
  }

  public static Complex operator +(Complex c1, Complex c2)
  {
    return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
  }
}
リスト11-1 演算子オーバーロード

 ユーザー定義型に対して、+などの演算子をオーバーロードにより定義できる。しかし、任意の型に対して演算子を定義すると複雑になりやすいため、複素数やベクトルといった限られた用途の型に対して定義することになるだろう。サンプルコードでは単項演算子-と二項演算子+のみをオーバーロードしている。

▲目次に戻る

コンストラクターとデストラクター

▲目次に戻る

インスタンス・コンストラクター

C#
class Program
{
  static void Main(string[] args)
  {
    var my1 = new MyClass();
    // 0
    var v1 = my1.Value;

    var my2 = new MyClass(2);
    // 2
    var v2 = my2.Value;
  }
}

public class MyClass
{
  public int Value { get; set; }

  // 引数なしのインスタンス・コンストラクター
  public MyClass() : this(0)
  { }

  // 2つ目のコンストラクター。引数あり
  public MyClass(int v)
  {
    Value = v;
  }

}

public class Ex12_1_InstanceConstructor_MyClass3
{
  private static int counter = 0;
  public Ex12_1_InstanceConstructor_MyClass3() => ++counter;
}
リスト12-1 インスタンス・コンストラクター

 ユーザー定義型をインスタンス化する処理としてインスタンス・コンストラクターを定義できる。インスタンス・コンストラクターは何も定義しない場合、引数なしのコンストラクターを利用できる。

 インスタンス内に複数のコンストラクターがある場合、循環参照にならない限り、別のコンストラクターをthisキーワードで呼び出すことができる。

 C# 7.0からは、コンストラクターを式形式で記述できるようになった。

▲目次に戻る

静的コンストラクター

C#
class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    // 5
    var v = my.Value;
  }
}

public class MyClass
{
  // 静的なメンバー
  static int DefaultValue;

  // 静的コンストラクター
  static MyClass()
  {
    // 代入するだけであればフィールドの初期化子として書けるが
    // より複雑な初期化も可能
    DefaultValue = 5;
  }

  public int Value { get; set; } = DefaultValue;
}

public static class Ex12_2_StaticConstructor_MyClass2
{
  private static int counter = 0;
  private static string counterValue;
  // C#7.0からは式形式で記述可能
  static Ex12_2_StaticConstructor_MyClass2() => counterValue = counter.ToString();
}
リスト12-2 静的コンストラクター

 ユーザー定義型の静的なメンバーを初期化する際に、静的コンストラクターを利用できる。静的コンストラクター内では静的なメンバーの初期値を代入できるため、インラインでは記述ができないような初期値の計算に利用できる。

 なお、静的コンストラクターはクラスがインスタンスされる時もしくは静的メンバーが参照される時に1回だけ実行される。

 C# 7.0からは、静的コンストラクターが式形式で記述できるようになった。

▲目次に戻る

デストラクター

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); // Destruct
  }
}

public class MyClass
{
  // デストラクター
  ~MyClass()
  {
    Console.WriteLine("Destruct");
  }
}

public class Ex12_3_Destructor_MyClass2
{
  private static int counter = 0;
  // C#7.0からは式形式で記述可能
  ~Ex12_3_Destructor_MyClass2() => --counter;
}
リスト12-3 デストラクター

 デストラクターはインスタンスが破棄される時に呼び出される処理を記述できる仕組みである。正確にはオブジェクトがGC(ガベージ・コレクション)により回収され、ファイナライザーを呼び出す際に実行されるため、実行されるタイミングを制御するのは困難だ。そのため、リソースを解放するような処理が必要な場合は、後述するusing構文を利用するためにIDisposableインターフェースを実装する方がいいだろう。

 C# 7.0からは、デストラクターが式形式で記述できるようになった。

▲目次に戻る

 以上、今回中編では、オブジェクト指向言語であるC#の要となる「クラス」関連の機能をできるだけコンパクトに説明した。次回後編では、前編と後編で取り上げていない残りの言語機能・仕様として、「タプルと分解(デコンストラクション)」「ローカル関数」「構造体」「継承とインターフェース」「列挙型」「イテレーター」「例外処理」「リソースの解放」「ラムダ式」「非同期処理(async/await)」について説明する。

1. C# 基礎文法【6.0対応】 ― 1回完結の最速再入門!

項目を羅列するだけでもかなり長くなってしまうC# 6.0の主要な文法を、実利用者目線でできるだけコンパクトにまとめた。日々のコーディングの「あれ、どう書くんだっけ?」を素早く解決するためのリファレンス。

2. C# 基礎文法 最速再入門【7.0対応】

「あれ、どう書くんだっけ?」を素早く解決するための、C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。前編では、C#の歴史/開発ツール/プログラムの実行と制御/型と変数/演算子/ステートメントを説明する。

3. 【現在、表示中】≫ C# クラスの基本機能 最速再入門【7.0対応】

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。中編では、名前空間/クラス/メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクターを説明する。

4. C# タプル/ローカル関数/ラムダ式/非同期処理/例外処理 最速再入門【7.0対応】

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。後編では、タプルと分解(デコンストラクション)/ローカル関数/構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放/ラムダ式/非同期処理(async/await)を説明する。

サイトからのお知らせ

Twitterでつぶやこう!