アセンブリの実行コードは共通中間言語(CIL, Common Intermediate Language)の形式で格納されます。 C#やVB.NETなど共通言語基盤(CLI, Common Language Infrastructure)の仕様に準拠する言語であればこれらのアセンブリを使用することができ、例えばC#で記述して作成したライブラリアセンブリを参照してVB.NETで使用するといったことが可能になります。

しかし、各言語がサポートする機能の差異により、ある言語で提供される機能が他の言語では使用できないといった場合が生じます。 ここでは、言語を跨って使用されるようなクラスライブラリを作成する上で重要となる言語間の相互運用性と共通言語仕様(CLS, Common Language Specification)について解説します。

§1 言語間の相互運用性

C#とVBでは、文法・構文はもちろん言語がサポートする機能にも違いがあります。 例えばポインタのサポートや大文字小文字の扱いなどです。 このような言語の違いに起因して発生しうる問題があります。 例として、C#でクラスを作成し、VBでそれを継承する場合を考えてみます。 まず、C#で次のようなクラスを作成します。

lib.cs (クラスライブラリとなるC#コード)
using System;

namespace SampleLibrary {
  public class Account {
    private int id;

    public void SetID(int newID) {
      setID(newID);
    }

    protected virtual void setID(int newID)
    {
      if (newID < 0) throw new ArgumentOutOfRangeException();

      id = newID;
    }

    public override string ToString()
    {
      return string.Format("ID={0}", id);
    }
  }
}

C#で書かれたAccountクラスは、IDを設定するためのパブリックなメソッドであるSetIDと、値をチェックしてフィールドにセットするメソッドsetIDを持っています。 setIDは派生クラスでチェック処理を追加したり動作をカスタマイズ出来るようにオーバーライド可能にしています。 C#ではメソッド名など識別子の大文字小文字の違いが区別されるため、このコードは問題なくコンパイル出来ます。

次に、VBでこのクラスを継承したクラスを作成することにします。

exe.vb (クラスライブラリを利用するVBコード)
Imports System
Imports SampleLibrary

Class AccountEx
  Inherits Account

  Protected Overrides Sub setID(ByVal newID As Integer)
    If newID = 0 Then Throw New ArgumentException("reserved ID")

    MyBase.setID(newID)
  End Sub
End Class

AccountExクラスでは、setIDメソッドをオーバーライドし、0番のIDの場合は予約済みのIDとして例外をスローするようにしています。 しかし、このコードをコンパイルしようとすると次のようなエラーが発生します。

大文字小文字だけが違うメンバをVBでオーバーライドしようとした場合
D:\clscompliant>csc /nologo /t:library /out:lib.dll lib\lib.cs

D:\clscompliant>vbc /nologo /out:sample.exe /r:lib.dll exe\exe.vb
D:\clscompliant\exe\exe.vb(7) : error BC31086: 'Overridable' として宣言されていないため、'Protected Overrides Sub setID(newID As Integer)' で 'Public Sub SetID(newID As Integer)' をオーバーライドすることはできません。

  Protected Overrides Sub setID(ByVal newID As Integer)
                          ~~~~~
D:\clscompliant\exe\exe.vb(7) : error BC30266: 異なるアクセス レベルが指定されているため、'Protected Overrides Sub setID(newID As Integer)' で 'Public Sub SetID(newID As Integer)' をオーバーライドすることはできません。

  Protected Overrides Sub setID(ByVal newID As Integer)
                          ~~~~~

VBでは識別子の大文字小文字の違いは区別されないため、プロテクトメソッドであるAccount.setIDとパブリックメソッドであるAccount.SetIDが区別されず、AccountExではパブリックメソッドであるAccount.SetIDをオーバーライドしようとしてこのようなエラーとなっています。

上記のような大文字小文字の違いの場合は別名のメソッドを提供することで解決することは出来ます。 しかし、極端な例になりますが、先のsetIDメソッドの機能をポインタを使ってしか操作できないように提供した場合は、VBではポインタをサポートしていないためメソッドをオーバーライドすることも呼び出すことも出来なくなってしまいます。

lib.cs (ポインタを使用するライブラリコード)
using System;

namespace SampleLibrary {
  public class Account {
    private int id;

    public void SetID(int newID)
    {
      unsafe {
        InternalSetID(&newID);
      }
    }

    protected virtual unsafe void InternalSetID(int* ptrNewID)
    {
      if (*ptrNewID < 0) throw new ArgumentOutOfRangeException();

      id = *ptrNewID;
    }

    public override string ToString()
    {
      return string.Format("ID={0}", id);
    }
  }
}
ポインタを使用するライブラリコードをVBから参照しようとした場合
D:\clscompliant>csc /nologo /unsafe /t:library /out:lib.dll lib\lib.cs

D:\clscompliant>vbc /nologo /out:sample.exe /r:lib.dll exe\exe.vb
D:\clscompliant\exe\exe.vb(7) : error BC30284: sub 'InternalSetID' はベース class の sub をオーバーライドしないため、'Overrides' として宣言することはできません。

  Protected Overrides Sub InternalSetID(ByVal newID As Integer)
                          ~~~~~~~~~~~~~
D:\clscompliant\exe\exe.vb(10) : error BC30657: 'InternalSetID' には、サポートされていない戻り値の型か、またはサポートされていないパラメータ型が指定されています。

    MyBase.InternalSetID(newID)
    ~~~~~~~~~~~~~~~~~~~~

このように、言語によって使用できる機能や仕様の違いが存在するため、ある言語でしか使えない機能を使って記述したライブラリは、他の言語では使えないという相互運用上の問題が発生します。



§2 共通言語仕様

このような問題を回避するためには、代替となる機能を提供するするか、どの言語でもサポートされているような機能のみを使うようにする必要があります。 .NET Frameworkでは、どの言語でも共通して用意されているべき基本的な機能を定めた共通言語仕様(CLS, Common Language Specification)というものが定められています。 共通言語仕様と個々の言語の仕様の関係を簡単な図で表すと次のようになります。

CLSと個々の言語仕様との関係

この共通言語仕様にしたがってクラスやメソッドをデザインすることにより、共通言語仕様に準拠した他の言語からでも使用できるように保証することが出来ます。 また、共通言語仕様(CLI)ではその一部として共通型システム(CTS, Common Type System)というものも定義しています。 これは、各言語の型に対して共通性を持たせるもので、クラス・インターフェイス・構造体・列挙体などの機能や特性、C#のintやVBのIntegerなどの組み込み型の扱いなどについてを定めています。 一つの例として、ある言語で作成したクラスを他の言語でも継承することがきるように保証するためには、この規則に従っている必要があります。

では、共通言語仕様では具体的にどのようなことが定められているのか見てみます。 以下の表は、MSDNの共通言語仕様より特に重要と思われるいくつかの規則を抜粋し補足を加えたものです。

CLSで定められている規則(抜粋)
機能 説明 補足
一般 参照可能範囲 CLS 規則は、型のうち、定義しているアセンブリの外部に公開される部分にだけ適用されます。 以下の規則は、パブリックなメソッドやクラスにのみ適用され、プライベートメソッドやinternalなクラスでは言語でサポートされるすべての機能を使うことが出来ます。
グローバル メンバ グローバルで静的な (static) フィールドとメソッドは CLS 準拠ではありません。 すべてのメソッド・変数は何らかの型に属している必要があります。
名前付け 文字および大文字と小文字の区別 2 つの識別子の区別には、大文字と小文字の相違は使用できません。 例えばIDListとIdListという二つのパブリックなクラスを作成することは出来ません。
キーワード CLS 準拠の言語コンパイラは、キーワードと一致してしまう識別子を参照できるようにする機構を提供する必要があります。また、CLS 準拠の言語コンパイラは、その言語のキーワードが名前になっている仮想メソッドを定義またはオーバーライドするための機構も提供する必要があります。 C#では@int、VBでは[Property]のように記述することでキーワードをエスケープすることが出来ます。
シグネチャ 型やメンバのシグネチャに含まれる戻り値の型およびパラメータの型はすべて CLS 準拠であることが必要です。 パブリックなクラスやメソッドでは、引数・戻り値もすべてCLS準拠である必要があります。
プリミティブ型 .NET Framework クラス ライブラリには、コンパイラが使用するプリミティブ データ型に対応する型が含まれています。これらの型のうち、Byte、Int16、Int32、Int64、Single、Double、Boolean、Char、Decimal、IntPtr、および String の型が CLS 準拠です。 UInt32やSByteはC#、VBなどでプリミティブ型としてサポートされていますが、これらはCLS非準拠の型となります。
コンストラクタの呼び出し コンストラクタは、継承されたインスタンス データにアクセスする前に、基本クラスのコンストラクタを呼び出す必要があります。 VBでMyBase.Newを呼び出す場合に最初のステートメントとして呼び出す必要があるのはこの規定に従うためです。
クラス型 継承 CLS 準拠クラスは、CLS 準拠クラスを継承する必要があります (System.Object は CLS 準拠です)。 CLSに準拠していないクラスを継承した場合はCLS非準拠とみなされます。
型のメンバ オーバーロード 演算子のオーバーロードは CLS 準拠にはなりません。ただし、CLS には、Add() などの有用な名前を付け、メタデータのビットを設定する方法についてのガイドラインが用意されています。演算子のオーバーロードをサポートするコンパイラはこのガイドラインに準拠することが推奨されますが、必須ではありません。 演算子をオーバーロードすると同時にAddやSubtractといった代替メソッドを用意することで、オーバーロードされた演算子をサポートしない言語にも同等の機能を提供することが出来ます。
変換演算子 op_Implicit または op_Explicit のいずれかがその戻り値の型でオーバーロードされた場合は、その代わりとなる変換方法を提供する必要があります。 上記の演算子の場合と同様、ToXxx/FromXxxといった型変換用の代替メソッドやコンストラクタを用意します。
ポインタ型 ポインタ ポインタ型および関数ポインタ型は CLS 準拠ではありません。 例えば、引数にポインタを取るメソッドを公開することは出来ませんが、メソッド内でポインタを使った演算を行うことやプライベートメソッドでポインタ型の引数を取ることは出来ます。
関数ポインタはMarshal.GetDelegateForFunctionPointerなどのメソッドを使ってデリゲートに変換することが出来ます。
配列 境界 配列の次元の下限値は 0 にする必要があります。 これはすべての言語で配列の最初のインデックスが0であることを意味します。
例外 継承 スローされるオブジェクトは、System.Exception 型であるか、または System.Exception から継承する必要があります。 数値や文字列を例外として直接スローすることは出来ず、Exceptionを継承したクラス(RuntimeWrappedExceptionなど)でラップする必要があります。
メタデータ CLS 準拠 型とそれらを定義するアセンブリとで、CLS 準拠の方法が異なる場合、それらの型を System.CLSCompliantAttribute でマークする必要があります。同様に、メンバとそれらの型とで CLS 準拠の方法が異なる場合には、それらのメンバもマークする必要があります。メンバまたは型に非 CLS 準拠のマークを付けた場合は、CLS に準拠した代替のメンバまたは型を提供する必要があります。 CLSCompliantAttributeについては後ほど詳しく解説します。
機能 説明 補足

§3 属性によるCLS準拠のチェック (CLSCompliantAttribute)

CLSに準拠することにより言語間の相互運用性が保証されるようになりますが、仕様をすべて把握していても誤ってCLS非準拠なコードを書いてしまう可能性はあります。 System.CLSCompliantAttributeを使うと、コンパイルの時点でコードがCLSに準拠しているかどうかチェックすることが出来ます。 この属性は、アセンブリや外部に公開されるクラス・メソッドなどに適用することでその要素をCLS準拠とするかどうかを指定し、CLS準拠とする場合はコンパイラに準拠しているかどうかをチェックさせる事が出来ます。

次の例は、アセンブリにCLSCompliantAttributeを指定し、アセンブリ全体でCLSに準拠するように指定した例です。 次のコードをコンパイルすると、コンパイラはアセンブリ全体でCLSに準拠しているかどうかをチェックします。 しかし、引数にCLS非準拠の型uintを取るメソッドが存在するため、警告を表示するようになります。

lib.cs (アセンブリに対してCLSCompliant属性を指定したライブラリコード)
using System;

[assembly: CLSCompliant(true)] // アセンブリがCLSに準拠するようにする (コンパイル時にCLS準拠のチェックが行われる)

namespace SampleLibrary {
  public class Account {
    private uint id;

    public void SetID(uint newID)
    {
      id = newID;
    }

    public override string ToString()
    {
      return string.Format("ID={0}", id);
    }
  }
}
コンパイル結果
D:\clscompliant>csc /nologo /target:library lib.cs
lib.cs(11,23): warning CS3001: 引数の型 'uint' は CLS に準拠していません。

CLSCompliantAttributeのコンストラクタに指定している値から想像できるように、CLSCompliantAttributeを指定した属性をCLS準拠とするかどうかを指定できます。 これにより、例えばアセンブリ全体ではCLS準拠としたいが、利便性のためにCLS非準拠なメソッドも提供したいといった場合にはCLSCompliantAttributeにfalseを指定することが出来ます。

次の例では、uintを引数にとるCLS非準拠なメソッドと、その代替となるlongを引数にとるCLS準拠なメソッドの二つを用意しています。 このコードをコンパイルしても、先の例とは異なり警告は発生しません。

CLS準拠のアセンブリの一部にCLS非準拠なメソッドを含める
using System;

[assembly: CLSCompliant(true)] // アセンブリがCLSに準拠するようにする

namespace SampleLibrary {
  public class Account {
    private uint id;

    [CLSCompliant(false)] // このメソッドはCLS非準拠とする
    public void SetID(uint newID)
    {
      id = newID;
    }

    // 上記の代替となるCLS準拠のメソッド
    public void SetID(long newID)
    {
      if (newID < 0) throw new ArgumentOutOfRangeException("newID", "負の値を指定することは出来ません。");

      id = (uint)newID;
    }

    public override string ToString()
    {
      return string.Format("ID={0}", id);
    }
  }
}

§4 演算子のオーバーロードと代替メソッドについてのメモ

C#およびVBでは演算子のオーバーロードをサポートしています。 また、CLSに準拠したクラスライブラリでは、演算子をオーバーロードする場合は演算子のオーバーロードをサポートしていない言語のために代替メソッドを用意することが推奨されます。

このこととは別に、演算子のオーバーロードを含む型をコンパイルすると、加算や型変換など演算子の実装を含む特別なメソッドが自動的に生成されます。 例えば加算演算子ではop_Addition、明示的な型変換演算子ではop_Explicitなどです(参考: 演算子のオーバーロード)。

そのため、自動生成された演算子のメソッドを呼び出すことが出来るならば、代替メソッドを作成することなくオーバーロードされた演算子の機能を使うことが出来ます。 以下は、演算子をオーバーロードした型を使い、オーバーロードされた演算子に該当するメソッドを直接呼び出す例です。

lib.cs (演算子をオーバーロードした型を含むライブラリコード)
using System;

[assembly: CLSCompliant(true)]

namespace SampleLibrary {
  // int型の値をラップする構造体
  public struct IntValue {
    private int val;

    public IntValue(int val)
    {
      this.val = val;
    }

    // 加算演算子
    public static IntValue operator+(IntValue x, IntValue y)
    {
      return new IntValue(x.val + y.val);
    }

    // 明示的な型変換演算子
    public static explicit operator int(IntValue x)
    {
      return x.val;
    }

    public override string ToString()
    {
      return val.ToString();
    }
  }
}
exe.cs (自動生成された演算子のメソッドを呼び出すコード)
using System;
using SampleLibrary;

class Sample {
  static void Main()
  {
    IntValue x = new IntValue(2);
    IntValue y = new IntValue(3);

    IntValue z = IntValue.op_Addition(x, y);

    Console.WriteLine("x + y: {0}", z);

    int i = IntValue.op_Explicit(x);

    Console.WriteLine("(int)x: {0}", i);
  }
}
ビルド・実行した結果
D:\clscompliant>csc /nologo /target:library lib.cs

D:\clscompliant>csc /nologo /r:lib.dll exe.cs
exe.cs(11,27): error CS0571: 'SampleLibrary.IntValue.operator +(SampleLibrary.IntValue, SampleLibrary.IntValue)':
        演算子またはアクセサを明示的に呼び出すことはできません。
exe.cs(15,22): error CS0571: 'SampleLibrary.IntValue.explicit operator int(SampleLibrary.IntValue)':
        演算子またはアクセサを明示的に呼び出すことはできません。

C#ではコンパイルエラーとなり演算子のメソッドを直接呼び出すことは出来ませんが、VBではエラーとなる事もなく演算子のメソッドを直接呼び出すことが出来ています。 言語でサポートしている必要があるためあまり推奨される方法ではありませんが、演算子のオーバーロードをサポートしていない言語からは、用意された代替メソッドではなく演算子のメソッドを直接呼び出させるようにすることも可能です。