目次

概要

C# は後方互換性に非常に注意を払っています。 C# 自身についても、C# のバージョンを上げることで動かなくなるコードが出ないように気を付けて機能追加をしていますし、 C# で書かれたライブラリについても、ライブラリ内の改修がライブラリ利用側で問題になりにくいように気を使って文法を決めています。

C# 自体の後方互換性

プログラミング言語に機能を追加する際、既存のプログラム コードがそのままコンパイルできるように互換性を保つことは非常に重要です。

文脈キーワード

C# 2.0 以降で追加されたキーワードは、全て文脈キーワード(contexual keyword) というものになっています。通常のキーワードとは違って、特定の文脈でしかキーワード扱いされません。

文脈キーワードの作り方にも数パターンがありますが、いくつか例を挙げて見ましょう。

  • yield: 2単語で初めてキーワード扱い
  • var: 変数宣言できる場所で、かつ、varという名前の型が存在しない時だけキーワード扱い
  • await: async修飾子が付いたメソッドの中でだけキーワード扱い
  • nameof: nameofという名前のメソッドがない時に限りキーワード扱い

yield

1つ目はC# 2.0で追加された「イテレーター」に関する yield キーワードです。 yield は、 yield return もしくは yield break という2単語並んだ状態でしかキーワード扱いされません。

ですので、C# 1.0時代に以下のようなコードを書いてた人がいたとしても、C# 2.0 以降でも問題なくコンパイルできます。

static void Calc(decimal dividends, decimal price)
{
    // yield には歩留まりとか出来高みたいな意味があって、
    // こういう変数名を使う人がいてもおかしくはない
    decimal yield = dividends / price;
    Console.WriteLine(yield);
}

極端な話、キーワードのyield (yield returnyield break)と並べて、型名や変数名でもyieldという識別子を使えます。

using System.Collections.Generic;

class Program
{
    static IEnumerator<yield> F()
    {
        // 「yield return」の2単語で初めてキーワードになる
        // 青いところだけがキーワード。
        // 水色が型名、黒が変数名。

        yield yield = 1;
        yield return yield;
    }

    struct yield
    {
        public int value;
        public static implicit operator yield(int n) => new yield { value = n };
    }
}

var

もう1つ、C# 3.0で導入された「型推論」に関する var キーワードは、変数宣言出来る文脈でだけキーワード扱いされます。 以下のようなコードも C# 3.0 でコンパイルできます。

static double Calc(IEnumerable<double> data)
{
    int count = 0;
    double sum = 0;
    double sqSum = 0;
 
    foreach (double x in data)
    {
        ++count;
        sum += x;
        sqSum += x * x;
    }
 
    // 分散(variance)。ローカル変数だし略して var って名前つける人はいる
    double var = (sum * sum - sqSum) / count;
    return var;
}

また、varという名前の型が存在していた場合は、型推論よりも優先的にそのvar型が使われます。

class Inferred
{
    static void F()
    {
        // この場合は型推論で Int 型の変数 var になる
        var var = 1;
    }
}

class SuccessfullyCompiled
{
    struct var
    {
        public int value;
        public static implicit operator var(int n) => new var { value = n };
    }

    static void F()
    {
        // この場合は ↑ の var 構造体型の変数 var になる
        var var = 1;
    }
}

class Erroneous
{
    struct var { }

    static void F()
    {
        // この場合は ↑ の var 構造体型になるけども、1 を代入できなくてコンパイル エラー
        var var = 1;
    }
}

C#では型名を小文字始まりにする習慣があまりないのでめったなことではこういう状態になりませんが、 もし万が一、C# 2.0以前にvar型を作っていた人がいてもちゃんとコンパイルできます。

逆に、あまり褒められた手法ではないですが、この仕様を逆手にとって、「このプロジェクトでは型推論を使わせない」というコーディング規約を遵守させるためにわざとvar型を定義しておく人もいるそうです。

await

C# 5.0 で導入された非同期メソッド用の await キーワードは、 「async 修飾子がついているメソッドの中でだけキーワード扱いされる」という方法で文脈キーワードになっています (async はメソッドの手前でだけキーワード扱い)。

static int X()
{
    var async = 2; // OK

    // 匿名関数の中などはまた別文脈
    // 匿名関数に async を付けているので、この中では await がキーワード
    Func<Task<int>> f = async () => { await Task.Delay(3); return async; };

    var await = 5; // OK
    return await * f().Result;
}

static async Task<int> XAsync()
{
    var async = 2;
    Func<Task<int>> f = async () => { await Task.Delay(3); return async; };
    var await = 5; // コンパイル エラー。キーワード扱いなので変数名に使えない。
    return await * await f();
}

非同期メソッドの場合、前述のyieldvarとは違い、もしもawaitという名前の型が存在していても、非同期メソッド内ではawaitはキーワードです。むしろ、await型の方を使うのにエスケープが必要です。

using System.Threading.Tasks;

class Program
{
    public struct await { }

    static async Task<int> XAsync()
    {
        // async が付いたメソッド内では ↑ の await 型は使えない
        var x = new await(); // コンパイル エラー

        // どうしても使いたかったら @ を付けてエスケープ
        var y = new @await(); // これならコンパイルできる
    }
}

ちなみに、C# 4.0以前には非同期メソッド自体がなかったので、これで破壊的変更になるソースコードはこの世に存在しないはずです。

また、asyncに関してもメソッド戻り値の手前でだけキーワード扱いされるので、例えば以下のようなコードでもちゃんとコンパイルできます。

using async = System.Threading.Tasks.Task;

class Program
{
    // 原理的には C# 4.0 時代にあり得るコード
    // ちゃんとコンパイル可能
    // この async は Task クラスのエイリアス
    static async F()
    {
        return async.Delay(1);
    }

    // ちゃんと、1つ目の async がキーワード、2つ目の async は型名
    static async async G()
    {
        await async.Delay(1);
    }
}

nameof

C# 6で導入されたnameof演算子は、同名のメソッドがない場合に限ってキーワード扱いされます。

using System;

class NoMethod
{
    static void F()
    {
        // nameof メソッドが存在しないのでこれはキーワード
        var x = 1;
        Console.WriteLine(nameof(x)); // x
    }
}

class SuccessfullyCompiled
{
    static void F()
    {
        // nameof メソッドがあるのでそちらが呼ばれてしまう
        var x = 1;
        Console.WriteLine(nameof(x)); // abc
    }

    static string nameof(int n) => "abc";
}

class Erroneous
{
    static void F()
    {
        // nameof メソッドがある上に、型が合わない
        // コンパイル エラーになる
        var x = 1;
        Console.WriteLine(nameof(x));
    }

    static string nameof(string s) => "";
}

メソッド名も、C#の習慣では大文字始まりで書くものなので、nameofメソッド(小文字始まり)を作って使っていた人はほとんどいないでしょう。 それでも万が一いたとしても、ちゃんとC# 6でコンパイルできます。

この仕様のため、1つ気を付けなければならないことがあります。 互換性的な問題ではないですが、using staticとの組み合わせで、 知らず知らずのうちにnameofメソッドが呼ばれる可能性があります。

using System;
using static MyExtensions;

class Program
{
    static void Main()
    {
        // 一見、nameof メソッドはなさそうに見えるけども…
        // using static MyExtensions; のせいで、MyExtensions.nameof が参照される
        var x = 1;
        Console.WriteLine(nameof(x)); // abc
    }
}

static class MyExtensions
{
    public static string nameof(object x) => "abc";
}

悪意を持ってわざとやらない限り書かれることはないであろうコードですが、一応注意してください。

余談1: 文脈依存の大変さ

文脈キーワードには、過去のバージョンとの互換性を取りやすいというだけでなく、識別子(変数名など)に使える単語が減らないという利点があります。 その一方で、キーワードかどうかをプログラム的に判別するのが難しくなり、例えば、ブログとかでのキーワードの色付け表示がしづらかったりします。 単にキーワードに色を付けるためだけでも単純な文字列マッチングではできず、C# の文法を理解する必要があります。

余談2: yield と await

いくつか紹介してきたように、文脈キーワードの作り方は一種類ではありません。 似たような機能であっても、文脈の作り方が異なる場合もあります。

例えば前述の通り、イテレーター用の yield は、2単語の複合キーワードにすることで文脈キーワードになっています。 一方で、awaitasync修飾子が付いたメソッド内では単独でキーワードになります。

static IEnumerable<int> Yield()
{
    var yield = 1; // OK
    yield return yield;
}

static async Task<int> Await()
{
    //var await = 1; // これはコンパイル エラー
    await Task.Delay(1);
    return 1;
}

似たような機能にも拘わらず異なる設計になっているのは、C# 2.0の時に導入したイテレーター構文にいくつか不満・不便があったからだそうです。

  • yield returnというように、2単語書くのがめんどくさい。
    • awaitは1単語。)
  • 匿名関数」内でyieldを使えない(匿名関数をイテレーター化できない)。
    • (非同期な匿名関数は作れる。)
  • メソッド内にyieldが含まれるかどうかによって、メソッド内部のコンパイル結果がまるっきり変わる(のが少し不気味)。
    • (一方、非同期メソッドの方は、await演算子を使わない限りasync修飾子を付けても付けなくてもコンパイル結果が同じという気持ち悪さはあります。)

やってできなくはないものの、コンパイラーの保守コストが跳ね上がって割に合わない。)

いまさら変更はできないんですが(もちろん互換性維持のため)、もしかすると、イテレーターも以下のように、別のキーワードで修飾するような文法の方がよかったかもしれません。

static iterator IEnumerable<int> Range(int from, int to)
{
    for (var i = from; i < to; i++)
        yield i;
}

C# で書かれたコードの互換性

C# の開発者は互換性に対して非常に多くの注意を払っています。 C# という言語自体の互換性だけでなく、 C# を使って書いたライブラリが互換性を保って利用してもらいやすいように C# の文法を決めています。

依存関係と、コード修正の影響

C# の文法の話をする前に、ライブラリの互換性維持について少し説明しておきましょう。 シンプルな例ですが、図1に、ライブラリの開発体制としてありがちな状況を示します。

ライブラリの開発体制の例
ライブラリの開発体制の例

例えばこれで、自分は真ん中の「自社製ライブラリ」の開発に関わっていることを想像してください。 自分たちが依存している他のライブラリもありますし、自分たちの作ったライブラリを利用しているアプリもあります。 直接ソースコードを修正できるのは自分たちの作っている「自社製ライブラリ」だけで、依存先の「他社製ライブラリ」は問題を見つけたとしてフィードバックをしてもすぐに修正される保証はありません。 利用者の「アプリ」に至っては、どこの誰が使っているのかさえわからない場合もあります(たとえ社内であったとしても部署が違えばよくある話)。

そして、「他社ライブラリ」中の親クラス(Base)を継承して、「自社ライブラリ」で子クラス(Derived)を作り、その子クラスを「アプリ」が使うというようなことも考えられます。 例えば以下のような状況です(わかりやすくするために1つにまとめていますが、Base、Derived、Program はそれぞれ別ファイル・別プロジェクトにあって、別の人が保守しているものと考えてください)。

using System;
 
// X さんが保守
class Base
{
    public void A() { Console.WriteLine("Base.A"); }
}
 
// Y さんが保守
class Derived : Base
{
    public void B() { Console.WriteLine("Derived.B"); }
}
 
// Z さんが保守
// X さん、Y さん、Z さんは互いに全く面識なし。
class Program
{
    static void Main()
    {
        var x = new Derived();
        x.A();
        x.B();
        Base y = x;
        y.A();
    }
}

この状況下で、Base や Derived クラスに対する修正がどういう影響を及ぼすかを考える必要があります。

基底クラスへの追加

当然ですが、public になっている部分を「変更」すると、利用側のコードが動かなくなります。 これはわかりやすい互換性の問題なので、たいていの開発者は細心の注意を払うと思います(変更したくてもしないとか、互換性を破棄する旨をあらかじめ伝えるとか)。

では、「追加」ならどうでしょう。以下のような場合がありえます。

変更前 変更後
class Base
{
    public void A() { Console.WriteLine("Base.A"); }
}
 
class Derived : Base
{
    public void B() { Console.WriteLine("Derived.B"); }
}
class Base
{
    public void A() { Console.WriteLine("Base.A"); }
 
    // 派生クラスに B メソッドがあることなんて知らないから足してしまった
    public void B() { Console.WriteLine("Base.B"); }
}
 
class Derived : Base
{
    // エラーにはならない。ただし、警告あり。
    public void B() { Console.WriteLine("Derived.B"); }
 
    // (別に問題ない場合)警告を消すためには public new void B() とする
    // (たいていは問題になったりするので早めにメソッド名を変えてしまえる方がいいのだけども)
}

Base 側に Derived 側と同じ名前のメソッドを追加してしまいました。 Base 側開発者は Derived 側のことを何も知らないので、悪意なく起こりえる話です。 この場合でも、C# はエラーを起こさないようにしています。

new 修飾子、override 修飾子

やむなく変更が必要な場合も考えてみましょう。 例えば以下の例を見てください。

変更前 変更後
class Base
{
    public void A() { Console.WriteLine("Base.A"); }
}
 
class Derived : Base
{
    // 意図して Base と同じ名前のメソッドを定義
    public new void A() { Console.WriteLine("Derived.B"); }
}
class Base
{
    public void ARenamed() { Console.WriteLine("Base.A"); }
}
 
class Derived : Base
{
    // 警告が出る。基底クラスに A がないのに new 修飾。
    // 少なくとも、Base 側の変更に気づきはする。
    public new void A() { Console.WriteLine("Derived.B"); }
}

Derived 側で A メソッドをわざわざ再定義(new)しているわけで、何らかの事情があったりします (普通はこういうコードは避ける。意図して(new 修飾子を付けてまで)やっている時点で何か事情がある)。 この場合だと、new 修飾子が不要なのについている状態になって、Derived 側開発者が Base 側の「事情が変わった」ということに気づけるようになっています。

もう1例、似たような話ですが、「仮想メソッド」の場合も見てみましょう。

変更前 変更後
class Base
{
    public virtual void A() { Console.WriteLine("Base.A"); }
}
 
class Derived : Base
{
    public override void A() { Console.WriteLine("Derived.B"); }
}
class Base
{
    public virtual void ARenamed() { Console.WriteLine("Base.A"); }
}
 
class Derived : Base
{
    // この場合はコンパイル エラー。
    public override void A() { Console.WriteLine("Derived.B"); }
}

仮想メソッドの場合、基底クラスと同じメソッドの別実装を与えることが目的なので、 そもそも基底クラスにないメソッドに override 修飾子がついているというのは明らかに何かのミスがあります。 なので、この場合は、コンパイル エラーを起こします(Base 側の変更に合わせて Derived 側も直す必要がある)。

オーバーロードの解決ルール

もう少し複雑な例を。 C# では、同じ名前で引数の型だけが違うメソッドを定義できます(「オーバーロード」)。 複数の候補がある場合には、もっとも型の一致度の高いものが選ばれます。 例えば以下のように、型がぴったり一致するオーバーロードがあればそちらが呼ばれます。

using System;
 
class Sample
{
    public void A(object x) { Console.WriteLine("object"); }
    public void A(string x) { Console.WriteLine("string"); }
}
 
class Program
{
    static void Main()
    {
        var x = new Sample();
        x.A(""); // A(string x) の方が呼ばれる
    }
}

ここでまた、基底クラスへのメソッド追加を考えてみましょう。

変更前 変更後
using System;
 
class Base
{
}
 
class Derived : Base
{
    public void A(object x) { Console.WriteLine("object"); }
}
 
class Program
{
    static void Main()
    {
        var x = new Derived();
        x.A(""); // 1個しかないので当然 A(object x) が呼ばれる
    }
}
using System;
 
class Base
{
    public void A(string x) { Console.WriteLine("string"); }
}
 
class Derived : Base
{
    public void A(object x) { Console.WriteLine("object"); }
}
 
class Program
{
    static void Main()
    {
        var x = new Derived();
        x.A(""); // 型の一致よりも、Derived にあることが優先されて、A(object) が呼ばれる
    }
}

通常のルールとは異なり、引数の型の一致度よりも、Derived 側で定義されているということの方が優先されます。

最初に説明した通り、Base 側と Derived 側は全く別の、それも面識のない開発者が保守している可能性があって、 Derived 側の事情はお構いなしに Base 側が変更される場合がありえます。 この場合、Base 側に後からメソッドを追加しても、元々の挙動が変わらないようにした結果として、こういうオーバーロードの解決順になっています。

C# における破壊的変更

注意は払っているといっても、C# にも破壊的変更(breaking change: 互換性を損ねる変更)がなくはないです。

粗を探せば結構な数があるものの、既存コード(マイクロソフト社内でのコードや、オープンソース プロジェクトのコード)を使って、問題のあるコードがほとんどないことを確認しているそうです。 実際、著者の知る範囲で、昔書いた C# コードが最新のコンパイラーでコンパイルして問題が起きたという体験をしたこと/聞いたことは1度もありません。

ここでは、一応、どんな破壊的変更があったのかを紹介しておきましょう (おそらく影響あるのは、ほとんどの人は思いつかないし、思いついても書かないようなコードだと思います)。

ジェネリクスの導入

Ver. 2.0

C# 2.0 で「ジェネリック」が導入されました。

ジェネリクスはほぼ上位互換な機能追加でしたが、 一応、やろうと思えば 1.0 でしかコンパイルできないようなコードが書けたりします。 以下のコードは、C# 1.0 でしかコンパイルできません。

using System;
 
class Program
{
    static void Main()
    {
        int x = 1;
        int y = 2;
        int z = 3;
        M(x < y, z > (0));
    }
 
    static void M(bool a, bool b) { Console.WriteLine("{0}, {1}", a, b); }
}

C# 1.0 では、このコードは2つの大小比較 x < yz > (0) を引数に与えるメソッド M 呼び出しとみなされます。

一方で、C# 2.0 以降の場合、x<y, z> というジェネリックなメソッドの呼び出しとみなされて、 「x はメソッドじゃない」「y, z という型はない」という理由でコンパイル エラーになります。

ジェネリックの変性と is 演算子

Ver. 4.0

is 演算子は、例外を出さずにキャストできるかどうかを判定する演算子です(参考: 「ダウンキャスト」)。

C# 4.0 で、ジェネリックに「変性注釈」を持たせれるようになったため、 キャストできるかどうかの結果が変わり、場合によっては互換性を失うコードがあります。 例えば以下のコードは、C# 4.0 以降では True、3.0 以前では False と表示されます。

using System;
using System.Collections.Generic;
 
class Program
{
    static void Main()
    {
        IEnumerable<string> x = new string[0];
        Console.WriteLine(x is IEnumerable<object>);
    }
}

実際に変性注釈を持てるようになったのは、「CLI」のレベルでは 2.0 の頃からでした(C# の文法に組み込まれたのが 4.0 から)。 ただし、IEnumerable<T> インターフェイスに out 修飾子が付いたのは .NET Framework 4 からなので、 このコードは、.NET 4 以降で実行するか、.NET 3.5 以前で実行するかによって結果が変わることになります(コンパイルに使った C# のバージョンでなく、実行に使う .NET Framework の方のバージョンに依存)。

自動実装 event

Ver. 4.0

C# 4.0 では、ひそかに自動実装イベント(add/remove アクセサーを持たないイベント。「イベント」参照)の内部実装方法が変更されました。 C# の仕様上、イベントの自動実装はスレッド安全であることを要請しています。 しかし、スレッド安全性を保証する方法はいくつかあり、C# 4.0 では、より安全でパフォーマンスもいい実装方法に変更されたという流れです。

例えば、以下のようなイベントがあったとします。

using System;
 
class Sample
{
    public event EventHandler<string> A;
}

C# 3.0 以前では、以下のような MethodImpl 属性を使ったコードに展開されていました。

using System;
using System.Runtime.CompilerServices;
 
class Sample
{
    public event EventHandler A
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        add { a = (EventHandler)Delegate.Combine(a, value); }
        [MethodImpl(MethodImplOptions.Synchronized)]
        remove { a = (EventHandler)Delegate.Remove(a, value); }
    }
    private event EventHandler a;
}

かつてはこれでよいと思われていたものの、今となっては、MethodImplOptions.Synchronized による同期にはいくつか問題が指摘されています (メソッド全体に lock(this) がかかるので、安全性的にもパフォーマンス的にもいまいち)。 そこで、C# 4.0 から、以下のようなコードが生成されるように変更されました。

using System;
using System.Threading;
 
class Sample
{
    public event EventHandler A
    {
        add
        {
            EventHandler a1, a2 = a;
            do
            {
                a1 = a2;
                var a3 = (EventHandler)Delegate.Combine(a1, value);
                a2 = Interlocked.CompareExchange(ref a, a3, a1);
            }
            while (a2 != a1);
        }
        remove
        {
            EventHandler a1, a2 = a;
            do
            {
                a1 = a2;
                var a3 = (EventHandler)Delegate.Remove(a1, value);
                a2 = Interlocked.CompareExchange(ref a, a3, a1);
            }
            while (a2 != a1);
        }
    }
    private event EventHandler a;
}

これは、lock ステートメント(それなりに負担が大きい機構)を使わずにスレッド安全性を保証する方法として知られているパターンの一種です。 基本的にはパフォーマンスがよくなっただけなので、変更といえど問題はほとんど起こりません。

問題が出る極端な場合を紹介すると、 Mono 2.10 以前のバージョンを使っていて、iOS 上で実行しようとした場合には、 CompareExchange メソッドが正しく動かないという問題があって、上記のコードが実行時エラーを起こします。 (あくまで、C# 4.0(Visual Studio 2010)以上を使って作った DLL を古いバージョンの Mono 経由で iOS 上で使おうとするという状況下でだけ起きる問題です。)

foreach の変数スコープ

Ver. 5.0

C# 5.0 で、foreach の仕様に変更がありました(参考「foreach の仕様変更」)。 以下のコードを実行すると、C# 4.0 以前と 5.0 以降で結果が変わります。

using System;
 
class Program
{
    static void Main()
    {
        Action a = null;
        foreach (var x in new[] { 1, 2, 3, 4, 5 })
        {
            a += () => Console.WriteLine(x);
        }
        a();
    }
}
4.0 以前 5.0 以降
5
5
5
5
5
1
2
3
4
5

4.0 以前では、あまりにも使い勝手が悪く、こういうコードを意図して書いている人はほぼいなくて、特に問題にならないという判断で仕様変更が行われました。

実際、4.0 からのバージョンアップで困ることはまずないでしょう。 ただし、その逆、C# 5.0 で作ったコードを古い環境に持っていってコンパイルしなおす場合には注意が必要です。 環境が混在している場合には特に注意しましょう。

C#と文字コード(カタカナ中点・)

Ver. 6

C# 6で、コンパイラーを1から作り直した影響もあって、C#コンパイラーが参照しているUnicodeのバージョンが変わりました。

ほとんどの場合、Unicodeのバージョンアップは文字の追加なので、破壊的変更になることはありません。しかし、1文字だけ、文字カテゴリーが変わって、今まで変数名につかえていたのに、C# 6からは変数名に使えなくなった文字があります。

詳しくは「注意: カタカナ中点」で説明していますが、カタカナ中点(なかぐろ)「・」(katakana middle dot、U+30FB)がその問題となる文字です。

ちなみに、C#的には、C#のどのバージョンがUnicodeのどのバージョンを使うかは特に明記せず、「とりあえずその時点で最新のUnicodeバージョンを使う」という方針になります。 (これまではマイクロソフト製C#コンパイラーは基本的にWindows上で動かすものだったので特に気にされることはありませんでしたが、C# 6以降の世代では、C#コンパイラーも.NETもオープンソース化、マルチプラットフォーム化した影響で、プラットフォームごとに多少、使える文字が変わる可能性があります。)

その他

その他細々と、破壊的変更に関する情報のまとめページを以下に掲載して起きます。

これまでに説明してきたような大きなもの以外では、バグっぽかったり仕様漏れだったものを直したものが多いです。

更新履歴

ブログ