CoClass属性

CoClassAttributeの本来の目的であるCOM interopはさておき、CoClass属性を使うと、次の例のように見かけ上インターフェイスを直接newする、インターフェイスに対してデフォルト実装クラスを定義するようなことができる。

CoClassAttributeを使ってインターフェイスのデフォルト実装クラスを定義する
using System;
using System.Runtime.InteropServices;

[CoClass(typeof(C))] // クラスCをインターフェイスIのコクラスとして定義する
[ComImport]
[Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")]
interface I {
  void M();
}

// インターフェイスIを実装するクラス
class C : I {
  public void M() => Console.WriteLine("Hello, world!");
}

public static class Sample {
  static void Main()
  {
    // インターフェイスIに対してnewする
    var i = new I();

    // 作成したインスタンスのメソッドMを呼び出す
    i.M();

    // インスタンスiの型情報を取得する (クラスCの型情報が取得される)
    Console.WriteLine($"type of {nameof(i)} is {i.GetType()}");
  }
}
CoClassAttributeを使ってインターフェイスのデフォルト実装クラスを定義する
Imports System
Imports System.Runtime.InteropServices

<
  CoClass(GetType(C)), ' クラスCをインターフェイスIのコクラスとして定義する
  ComImport,
  Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")
>
Interface I
  Sub M()
End Interface

' インターフェイスIを実装するクラス
Class C
  Implements I

  Public Sub M() Implements I.M
    Console.WriteLine("Hello, world!")
  End Sub
End Class

Public Module Sample
  Sub Main()
    ' インターフェイスIに対してNewする
    Dim i As New I()

    ' 作成したインスタンスのメソッドMを呼び出す
    i.M()

    ' インスタンスiの型情報を取得する (クラスCの型情報が取得される)
    Console.WriteLine($"type of {NameOf(i)} is {i.GetType()}")
  End Sub
End Module
実行結果
Hello, world!
type of i is C

インターフェイスIに対するnewは、コンパイル時において、CoClass属性で指定されたコクラスCに対するnewに置き換えられる。


シグニチャがマッチする、かつアクセス可能なコンストラクタが存在する場合は、引数付きでnewすることもできる。 引数リストと一致するアクセス可能なコンストラクタがなければコンパイルエラーとなる(実行時例外ではない)。

CoClassAttributeで定義したデフォルト実装クラスと引数付きのコンストラクタ
using System;
using System.Runtime.InteropServices;

[CoClass(typeof(C))]
[ComImport]
[Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")]
interface I {
  void M();
}

class C : I {
  // 引数を取るコンストラクタ
  public C(int x, string y, object z) => Console.WriteLine($"x = {x}, y = {y}, z = {z}");

  public void M() => Console.WriteLine("Hello, world!");
}

public static class Sample {
  static void Main()
  {
    new I(42, "foo", "bar");
  }
}

CoClassAttributeで定義したデフォルト実装クラスと引数付きのコンストラクタ
Imports System
Imports System.Runtime.InteropServices

<
  CoClass(GetType(C)),
  ComImport,
  Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")
>
Interface I
  Sub M()
End Interface

Class C
  Implements I

  Public Sub New(ByVal x As Integer, ByVal y As String, ByVal z As Object)
    Console.WriteLine($"x = {x}, y = {y}, z = {z}")
  End Sub

  Public Sub M() Implements I.M
    Console.WriteLine("Hello, world!")
  End Sub
End Class

Public Module Sample
  Sub Main()
    Dim i As New I(42, "foo", "bar")
  End Sub
End Module
実行結果
x = 42, y = foo, z = bar

アセンブリ境界を跨ぐ場合、コクラスにアクセスできなければコンパイルエラーとなる(実行時例外ではない)。 このため、ライブラリにてインターフェイスとデフォルト実装(CoClass属性)のみを公開しつつ、デフォルト実装クラスは非公開とする、といったことはできない。

ライブラリ側コード
using System;
using System.Runtime.InteropServices;

[assembly: System.Reflection.AssemblyTitle("Library")]

namespace Library {
   // コクラスを指定したpublicなインターフェイス
  [CoClass(typeof(C))]
  [ComImport]
  [Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")]
  public interface I {
    void M();
  }

  // コクラス(internalなクラス)
  internal class C : I {
    public void M() => Console.WriteLine("Hello, world!");
  }
}
アプリケーション側コード
// <ProjectReference Include="Library.csproj" />

using System;

[assembly: System.Reflection.AssemblyTitle("Application")]

namespace Application {
  public static class Sample {
    static void Main()
    {
      var i = new Library.I(); // error CS0122: 'C.C()' はアクセスできない保護レベルになっています

      i.M();
    }
  }
}

ComImport属性とGuid属性

C#でCoClass属性をインターフェイスに付与する場合、ComImport属性Guid属性も同時に付与する必要がある。 Guid属性で指定するGUIDは適当でよい(COM importを目的としないのであれば)。

C#では、CoClass属性はComImport属性とGuid属性とともに指定する必要がある
[CoClass(typeof(C))]
/*[ComImport]*/ // warning CS0684: 'I' インターフェイスは、'CoClassAttribute' でマークされていますが、'ComImportAttribute' ではマークされていません。
/*[Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")]*/ // error CS0596: Guid 属性は ComImport 属性を使って指定する必要があります。
interface I {
  void M();
}

VBでCoClass属性をインターフェイスに付与する場合は、ComImport属性とGuid属性は必ずしも付与する必要がなく、コンパイル時に自動的に付与されるわけでもない模様。

VBでは、CoClass属性は必ずしもComImport属性とGuid属性を伴っていなくてもよい
Imports System
Imports System.Runtime.InteropServices
Imports System.Reflection

<CoClass(GetType(C))> '  ComImport属性とGuid属性がなくてもコンパイルできる
Interface I
  Sub M()
End Interface

Class C
  Implements I

  Public Sub M() Implements I.M
    Console.WriteLine("Hello, world!")
  End Sub
End Class

Public Module Sample
  Sub Main()
    Dim i As New I()

    Console.WriteLine($"Guid = {i.GetType().GetCustomAttribute(Of GuidAttribute)()}")
    Console.WriteLine($"ComImport = {i.GetType().GetCustomAttribute(Of ComImportAttribute)()}")
  End Sub
End Module
実行結果
Guid = 
ComImport = 

インターフェイスメソッドのデフォルト実装との比較

CoClassによるデフォルト実装クラス
using System;
using System.Runtime.InteropServices;

// デフォルト実装クラスを持つインターフェイス
[CoClass(typeof(C))]
[ComImport]
[Guid("2964ca28-75b5-4eba-9472-e2aa977b5cf3")]
interface I {
  void M();
}

// インターフェイスIを実装するクラス
class C : I {
  public void M() => Console.WriteLine("Hello, world!");
}

public static class Sample {
  static void Main()
  {
    // インターフェイスIに対してnewする
    var i = new I();

    // 作成したインスタンスのメソッドMを呼び出す
    i.M();

    // インスタンスiの型情報を取得する
    Console.WriteLine($"type of {nameof(i)} is {i.GetType()}");
  }
}
実行結果
Hello, world!
type of i is C
インターフェイスメソッドのデフォルト実装
using System;


// メソッドのデフォルト実装を持つインターフェイス



interface I {
  void M() => Console.WriteLine("Hello, world!");
}

// インターフェイスIを実装するクラス
class C : I {

}

public static class Sample {
  static void Main()
  {
    // クラスCのインスタンスを作成し、インターフェイスIに変換する
    I i = new C();

    // 作成したインスタンスのメソッドMを呼び出す
    i.M();

    // インスタンスiの型情報を取得する
    Console.WriteLine($"type of {nameof(i)} is {i.GetType()}");
  }
}
実行結果
Hello, world!
type of i is C

参考