Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 

C# 早わかりリファレンス

― 5/6ページ: 構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放 ―

2024年10月10日

構造体

構造体

C#
class Program
{
  static void Main(string[] args)
  {
    var p1 = new Point(10);
    // 参照ではなく、コピーした値がp2に割り当てられる
    var p2 = p1;
    // p1のプロパティを更新してもp2には影響がない
    p1.X = 1;
    // p1.X=1
    // p2.X=10

    // 構造体の初期値はnullにはなり得ない
    var points = new Point[10];
    // point[0] != null
    // point[0].X == 0  // 各フィールドはデフォルト値で初期化される

    // [参考]一方、クラスの初期値はnullである
    var uris = new System.Uri[10];
    // uris[0] == null
  }

  struct Point
  {
    private int x;
    public int X
    {
      get { return x; }
      set { x = value; }
    }
    public Point(int x)
    {
      this.x = x;
    }
  }
}
リスト13-1 構造体

 構造体は、クラスによく似た構造だが、値型でありヒープ割り当てを必要としない。そのため、値を持つ小規模なデータ構造に適している。

 構造体は値型であるため、構造体を参照する変数を別の変数に割り当てると、参照ではなくコピーした値が代入される。サンプルコードでは、変数p1p2に代入しているが、その後p1のプロパティを変更してもp2は変更されない。

 また、値型であるため、構造体自身の初期値はnullではなく、構造体の各フィールドはそれぞれのデフォルト値で初期化した値となる。そのため構造体を要素とする配列を初期化した時点で、配列の各要素には構造体の初期値が代入されている。

継承とインターフェース

継承と仮想overridenew

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass1();
    my.Test2(); // MyClass1.Test2

    var my2 = new MyClass2();
    my2.Test2(); // 1
  }
}

public class MyBase
{
  public int Value { get; set; } = 0;

  public void Test1()
  {
    Console.WriteLine("MyBase.Test1");
  }

  public virtual void Test2()
  {
    Console.WriteLine("MyBase.Test2");
  }
}

public class MyClass1 : MyBase
{
  public new int Value { get; set; } = 1;

  public override void Test2()
  {
    Console.WriteLine("MyClass1.Test2");
  }
}

public class MyClass2 : MyClass1
{
  public override void Test2()
  {
    Console.WriteLine(Value);
  }
}
リスト14-1 継承

 オブジェクト指向の特徴の一つであるクラスの継承は、クラス名の後に:基底クラスを宣言することで記述できる。C#ではvirtual省略したメソッドは、非仮想メソッドであり継承したクラスでオーバーライドすることはできない。そのため、オーバーライドを許可するメソッドは基底クラスの側でvirtual修飾子を付けてオーバーライドを許可する必要がある。オーバーライドする側のメソッドはoverride修飾子を付けるが、さらに継承先のクラスでオーバーライドすることもできる。

 また、継承したクラスで基底クラスのメソッドやプロパティと同じ名前のメンバーを宣言することも可能だ。このとき、基底クラスのメンバーは隠ぺいされてしまうため、継承したクラスではメソッドやプロパティなどのメンバーにnew修飾子を付けて隠ぺいしていることを明示できる。

抽象クラスシールクラスシールメソッド

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.Test1(); // MyAbstractClass.Test1
    my.Test2(); // MyClass.Test2
    my.Test3(); // MyClass.Test3

    var my2 = new MyClass2();
    my2.Test1(); // MyAbstractClass.Test1
    my2.Test2(); // MyClass2.Test2
    my2.Test3(); // MyClass.Test3
  }
}

public abstract class MyAbstractClass
{
  public void Test1()
  {
    Console.WriteLine("MyAbstractClass.Test1");
  }

  public abstract void Test2();

  public abstract void Test3();
}

public class MyClass : MyAbstractClass
{
  public override void Test2()
  {
    Console.WriteLine("MyClass.Test2");
  }

  public sealed override void Test3()
  {
    Console.WriteLine("MyClass.Test3");
  }
}

public sealed class MyClass2 : MyClass
{
  public override void Test2()
  {
    Console.WriteLine("MyClass2.Test2");
  }
}
リスト14-2 抽象クラスとシールクラス、シールメソッド

 抽象クラスは直接インスタンス化できないクラスで、通常のメソッド定義に加え抽象メソッド抽象プロパティをメンバーに持つことができる。これは継承先のクラスが持つ共通の振る舞いをあらかじめ定義しておくときに役立つ。抽象メソッドを継承したクラスでオーバーライドするときはoverride修飾子を付ける。

 抽象クラスからの継承に限らず、定義したクラスを継承することを禁止する場合、sealed修飾子を付けてシールクラスにすることができる。また、メソッド単位でオーバーライドを禁止する場合も、sealed修飾子を付けてシールメソッドにすることができる。

インターフェースと明示的実装

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var s1 = new Surface();
    s1.Name = "a";
    s1.Paint(); // Paint
    ((ISurface)s1).Paint(); // Paint
    ((IPaintable)s1).Paint(); // Paint

    var s2 = new Surface2();
    s2.Paint(); // Paint
    ((ISurface)s2).Paint(); // Paint
    ((IPaintable)s2).Paint(); // IPaintable.Paint
  }
}
interface IPaintable
{
  void Paint();
}

interface ISurface
{
  string Name { get; set; }
  void Paint();
}

public class Surface : ISurface, IPaintable
{
  public string Name { get; set; }
  public void Paint()
  {
    Console.WriteLine("Paint");
  }
}

public class Surface2 : ISurface, IPaintable
{
  public string Name { get; set; }
  public void Paint()
  {
    Console.WriteLine("Paint");
  }

  void IPaintable.Paint()
  {
    Console.WriteLine("IPaintable.Paint");
  }
}
リスト14-3 インターフェースと明示的実装

 インターフェースはメソッドやプロパティのコントラクト(=実装すべき規約)を定義でき、インターフェースを実装するクラスや構造体は、必ずそのコントラクトに従って実装する必要がある。C#では継承するクラスは1つのみだが、インターフェースは複数実装できる。

 インターフェースを複数実装する場合、異なるインターフェースが同じコントラクトを持っていることがある(サンプルコードではIPaintableISurfaceインターフェースの両方にvoid Paint()メソッドがある)。この場合、特に指定せずに実装すると両者のインターフェースは同じ実装メソッドを呼び出す。しかし、それぞれのインターフェースで異なる実装を行いたい場合は、明示的に実装可能だ。サンプルコードのSurface2クラスのIPaintable.Paintメソッドがその例で、IPaintableインターフェースにキャストして呼び出すと、明示的実装が呼び出されることになる。

列挙型

列挙型の宣言

C#
class Program
{
  static void Main(string[] args)
  {
    var c1 = Color.Red;
    var c2 = Color.Blue;
    // 定義されていない値をキャスト可能だが、非推奨
    var t3 = (LongType)23;
  }
}
public enum Color
{
  Red,
  Blue,
  Orange
}

public enum LongType : long
{
  Solid = int.MaxValue + 1L,
  Soft = 0,    // 必ず0を定義することが推奨される
  Hard = Solid
}
リスト15-1 列挙型

 列挙型(=列挙体)は、定数のリストを名前付きで管理するための構造である。例えばリスト15-1のColor列挙型の各値には名前を付けているが、その実体はint型もしくはlong型の数値である。数値を明示的に指定しない場合は、宣言された順に0から1つずつ増えた値が割り当てられる。明示的に指定する場合、0が割り当てられる列挙型の値を必ず定義することが推奨されている。

 列挙型は数値であるため、数値を列挙型にキャストでき、実際には定義されていない値であってもキャスト可能である。しかし、これは予期しない実行時エラーを起こしかねないので推奨されていない。

Flags属性

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var colors = Color.Red | Color.Blue;
    colors.HasFlag(Color.Red); // True
    var f1 = (colors == Color.Red); // False
    colors.HasFlag(Color.Yellow); // False
  }
}

[Flags]
public enum Color
{
  Red,
  Blue,
  Yellow
}
リスト15-2 Flags属性

 列挙型の値を利用する際に、複数の値を持っている状態を表現したい場合がある。その場合は、列挙型の宣言にFlags属性を付けることでビットフラグを表現できる。複数の値を持つ値は|演算子で記述でき、複数とり得る値の中で指定した列挙型の値が含まれているかどうかは、HasFlagメソッドで検査できる。

イテレーター

イテレーターの宣言とyield

C#
using System;
using System.Collections;
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    // a b c z
    var samples = MyCollection.GetSamples();
    // 1, 9, 25, 49, 81
    foreach (var element in new MyCollection())
    {
      Console.WriteLine(element);
    }
  }
}
public class MyCollection : IEnumerable<int>
{
  public static IEnumerable<string> GetSamples()
  {
    yield return "a";
    yield return "b";
    yield return "c";
    yield return "z";
  }

  public IEnumerator<int> GetEnumerator()
  {
    for (int i = 0; i < 10; i++)
    {
      if (i % 2 == 0)
        continue;

      yield return i * i;
    }
  }

  // IEnumerableインターフェースのGetEnumerator()メソッドを実装
  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
}
リスト16-1 イテレーター

 返り値がIEnumerable型およびそのジェネリスク型のIEnumerable<T>である場合、yield returnキーワードで反復表現を記述できる。yield returnするたびにIEnumerable型オブジェクトの要素1つを返すことができ、メソッドが完了した時点で要素の列挙が終わったことになる。

 また、クラスがIEnumerableインターフェースもしくはそのジェネリクス型のインターフェースを実装する場合の、GetEnumeratorメソッドの実装にもyield returnを実装できる。

例外処理

throwとtry-catch

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    try
    {
      ThrowException();
    }
    catch (MyException ex) when (ex.Value >= 1)
    {
      Console.WriteLine("Catch MyException");
    }
    catch (Exception ex)
    {
      Console.WriteLine("Catch Exception: " + ex.Message);
      throw;   // 再スロー
    }
  }
  static void ThrowException()
  {
    throw new MyException() { Value = 1 };
  }
}
public class MyException : Exception
{
  public int Value { get; set; }
}
リスト17-1 例外処理

 例外の発生をthrow句で記述できる。C#ではメソッド宣言にスローされる例外を記述することはできず、任意の例外がスローされる可能性がある。スローする例外は、定義済みの例外もしくはExceptionクラスを継承したユーザー定義の例外をスローできる。

 例外処理はtry-catch構文で記述し、tryブロックの中で発生した例外をcatchブロックで処理できる。catchブロックは、まず例外の型により複数宣言でき、スローされた例外が代入可能な最初のcatchブロックが実行される。代入可能なcatchブロックがない場合はメソッド呼び出し元に例外がスローされる。C# 6.0からcatchブロックにwhenキーワードでさらにcatchするかどうかの条件を記述できるようになった。

 catchブロック内で例外を再スローしたい場合は、throw句を記述する。throw句の後の例外インスタンスは省略でき、省略した場合は呼び出し階層を記録したスタックトレースが複雑にならないため、特別の理由がない限りは例外インスタンスを省略した方がいいだろう。

リソースの解放

finallyとusingによるリソースの解放

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var resource = new MyResource();
    try
    {
      resource.Execute();
    }
    finally
    {
      resource.Close();
    }

    using (var r1 = new MyDisposableResource())
    using (var r2 = new MyDisposableResource())
    {
      r1.Execute();
      r2.Execute();
    }
  }
}
public class MyResource
{
  public void Execute()
  {
    Console.WriteLine("MyResource.Execute");
  }

  public void Close()
  {
    Console.WriteLine("MyResource.Close");
  }
}

public class MyDisposableResource : IDisposable
{
  public void Execute()
  {
    Console.WriteLine("MyDisposableResource.Execute");
  }

  public void Dispose()
  {
    Console.WriteLine("MyDisposableResource.Dispose");
  }
}
リスト18-1 リソースの解放

 利用したインスタンスに対し、リソースの解放などのために特定のメソッドを呼び出す必要がある場面を考えよう。例外がスローされる状況でも必ず解放処理を行うために、try-finally句を使うことができる。finally句内の処理は例外が起きた場合でも必ず実行される。しかしこの構文は少し冗長であり、変数のスコープもtryの外側に及んでしまう。

 そのため、C#ではusingステートメント(前述のusingディレクティブとは異なるキーワード)が用意されている。IDisposableインターフェースを実装したクラスはusing内に記述でき、ブロックを抜ける際に必ずその実装クラスのDispose()メソッドが呼ばれるようになっている。

サイトからのお知らせ

Twitterでつぶやこう!