C# は、進化していくにあたって、破壊的変更を極力起こさないようにかなり気を使っているプログラミング言語です。 細かい話をすると破壊的変更も皆無ではないんですが、 破壊的変更を認める(認めてでも追加したい新機能を実装する)ハードルは結構高めです。
そんな C# ですが、ちょっとそのハードルの基準を緩められないかというような話が出ています。
補足: 影響範囲と、影響力の軽減
補足として、 ハードルを緩めるといっても本当にちょっとです。 C# チームは、「GitHub の public リポジトリを検索して、実際に影響を受けたコードを探す」とかやって既存のコードに対する影響を評価してたりするんですが、
- これまで: 単体テストとかでわざと変なコードを書いているものを除いて、ほぼ影響皆無なら OK
- 提案: それほど多くはないものの、無視できると言えるほど皆無ではないものでも OK にしたい
みたいな感じ。
代わりといってはなんですが、影響を受ける人への負担を最小限にするために、以下のような仕組みを提供するのはどうか?という提案になっています。
- 言語バージョンを最新のものにアップグレードすると影響を受けるコードを識別する
- そういうコードに対して診断メッセージを出して、破壊的変更があることを知らせる
- 自動コード修正機能で、破壊的変更を受けないようなコードへの書き換えを提供する
- 早い段階でこれらの診断・コード修正を提供する
これまでの破壊的変更の例
件の discussionで触れられているわけではないですが、 補足的に、 これまでの「ほぼ影響皆無」な破壊的について紹介しておきましょう。 細かく言うともっといろいろとあるんですが、結構大きめのもののみ抜粋。
ジェネリクスの <>
C# のジェネリクスは C# 2.0 からの導入なわけで、それ以前には M<T>()
みたいな <>
の用法はありませんでした。
ここで、多少工夫すると、C# 1.0 の頃でも合法そうな <>
が書けます。
例えばこんな感じ:
X(A<B, C>(D));
- C# 1.0 の解釈: 2引数のメソッド
X
があって、式A<B
とC>(D)
が引数 - C# 2.0 の解釈: 1引数のメソッド
X
と、引数1つで型引数2つのメソッドA
がある
色が付くと多少わかりやすいですかね。
// C# 1.0 解釈 X(A < B, C > (D)); // C# 2.0 解釈 X(A<B, C>(D));
まあ、狙わないと踏めないですね。 C# 2.0 当時に踏んだ人はいないんじゃないでしょうか。 実際僕も確か、C# 5.0 辺りの時に「かつてこんなのあったけども誰も気にしなかったよ」的な話題で知りました。
foreach 変数のキャプチャ
割かしちゃんとアナウンスがあった破壊的変更でいうと、C# 5.0 のときの foreach
の仕様変更があります。
詳細はリンク先を見てもらうとして、
簡単に言うと以下のコードの実行結果が C# 4.0 以前と 5.0 以降で変わります。
var data = new[] { 1, 2, 3, 4, 5 }; Action a = null; foreach (var x in data) { a += () => Console.WriteLine(x); } a();
x
のスコープが foreach
の内側か外側かが変わっていて、
単一の変数 x
が全てのループで共有されるか、ループごとに違う変数扱いになるかが変わります。
結果的に、(C# 4.0 以前)「5つの5が表示される」か(C# 5.0 以降)「1, 2, 3, 4, 5 が表示される」かという結構大きな差になります。
まあ、4.0以前の挙動の方をバグだと思う人もいたくらいです。 このコードを書いてみて5が5つ表示されたら、まあ、コードを書き換えますよね、普通。 なので、破壊的変更の影響を受ける人はほぼ皆無でした。
この当時はまだ「GitHub にあるコードをクロールして調べる」みたいな手段がなかったので、 C# チーム的には恐る恐る破壊的変更をリリースしていました。 ですが、まあ、結果的には「心配しすぎだった」と言われているくらい、不平不満の声はなかったはずです。 繰り返しになりますが、バグ修正とすら思われているレベルです。
record, required, scoped, file
C# 9.0 でrecord
が、
C# 11.0 でrequired
、scoped
、file
が新たにキーワードになりました。
ただ、幸い、これらは(当然、文脈キーワードで)「型名として使おうとする時だけまずい」という仕様になっています。
class A { // 全然平気。 int record; void M(int record) { } int M() { int record = 0; return record; } // これがダメ。 // 以前: record という名前のクラスのフィールド x // C# 11: x という名前のレコード型宣言 record x; } class record { }
幸い、C# では「型名は大文字始まりにする」という文化が浸透していて、わざわざこの規約に反する型名を使う人もほとんどいません。
昔ならそれでも破壊的変更はしり込みしたんでしょうが、 今回は「GitHub にあるコードをクロールして調べる」が有効に機能したようです。 調べた結果、デモやテストでわざと変な名前をつけている人を除いて、問題を起こしそうなコードは見当たらなかったそうです。
実際、C# 9.0 リリース後にこれで困ったという人は見かけません。
それもあってか、C# 11.0 では、そもそも required
、scoped
、file
という名前の型宣言自体エラーにしました。
結構な破壊的変更ですが、これで困ったという人は、僕の知る限りは見かけたことはありません。
(1個だけ、native interop で、native 側に file
という構造体がいて、
それに合わせて「C# でも意図的に小文字始まりの file
を使う」みたいな判断をしていたコードは見たことがあります。それは struct @file {}
と書けば解決。)
今懸念される新機能: 半自動プロパティ
今何で困っているかというと、1月にブログに書いた半自動プロパティです。
field
キーワードの追加。
class A { // 手動プロパティ (manual property) // (と、自前で用意したフィールド)。 // こういう、プロパティからほぼ素通しで値を記録しているフィールドを「バッキング フィールド」(backing field)という。 private int _x; public int X { get => _x; set => _x = value; } // 自動プロパティ (auto-property)。 // 前述の X とほぼ一緒。 // バッキング フィールドの自動生成。 public int Y { get; set; } // 【C# 12 候補】 半自動プロパティ (semi-auto-property)。 // バッキング フィールドは自動生成。 // 全自動の方と違って、バッキング フィールドの使い方は自由にできる。 // field キーワードでバッキング フィールドを読み書き。 public int Z { get => field; set => field = value; } }
record
とかと違ってこれが危ないのは、「field
という名前のフィールドがいたらアウト」という、割かしありそうなラインなせいです。
以下のコード、半自動プロパティが実装される前後で意味が変わる可能性が大きくなっています。
(回避できなくもないものの、コストが高すぎてできれば破壊的変更を認める方向で進めたい。)
class A { private int field; public int Property { get => field; set => field = value; } }
これはGitHubで調べたら、いるらしいです。 まあ、いそうですよね。
ただ、そんなに多くもない。
安直な field
という名前のフィールドがそこまで多くないというのもありますが、
C# のコーディング規約上の派閥的な話もあります。
フィールドの命名規約として「_
を付ける派」は影響を受けません。
class A { private int _field; // _ 派。影響を受けない。 public int Property { get => _field; set => _field = value; } }
「インスタンス メンバーには常に this.
を付ける派」も影響を受けません。
class A { private int field; public int Property { // this. 派。影響を受けない。 get => this.field; set => this.field = value; } }
C# は結構「private
なところのコーディング規約は口うるさく言わない」みたいなところがあるので、フィールドに関しては _field
、this.field
、field
の3つとも結構います。
さて、このラインの「大した影響ではないものの、無視できるほどは皆無じゃない」をどう扱いましょうか。 というのが現在の課題。
さかのぼって
「field
フィールド」程度の破壊的変更を認めたいのであれば、
過去のさかのぼれば、同程度の以下の影響範囲だけどもちょっと特殊対応して破壊的変更を避けたものがあります。
var
:var
という名前の型がないときに限りキーワード扱いdynamic
:dynamic
という名前の型がないときに限りキーワード扱い_
: 1つも変数参照がないときに限り discard 扱い
特に前2者なんて、required
や scopde
が型名として使えなくなった今、かなり不自然ですよね。
かつては「型推論の var
を使わせないために、わざと class var {}
を定義しておく」という嫌がらせのような規約を定めてしまう人も一部いたそうですが。
今では「そんなことやるのは推奨されていない」で一蹴していいと思います。
改めて、破壊的変更の影響軽減
とりあえず差し当たっては「field
フィールド」問題、もしかするとさらに踏み込んで「var
型」問題を、今後、破壊的変更を認める方向で進めることになるかもしれません。
さすがにサイレントに行うには大きすぎる破壊的変更なので、以下のように進めたいとのこと。
-
コンパイラーを最新にした場合、言語バージョンを更新しなくても(TargetFramework を最新にしなくても)、「最新の C# で破壊的変更になる」旨を警告する
- 言語バージョンを上げるつもりのない人向けに、抑止オプションも提供する
-
自動コード修正を提供して、早期に修正してもらう
- 半自動プロパティの例でいうと
field
をthis.field
に自動的に置き換える
- 半自動プロパティの例でいうと
- このコード修正は IDE 上でも、コマンドラインでの実行もできるようにする
discussion での反応は「賛成多数」(👍100 対 👎4)。 むしろ、「他の言語はもっと破壊的変更してるだろ。やっちゃえ」発言も目立ちます。 ただ、この discussion に参加しに来る人はその時点で「積極的な人」のはずなので、 もう少しいろんな方面の調査は必要かと思われます。
また、1つ disucussion 内で挙げられた懸念として、 「StackOverflow とかからコピペしてくるコード問題」があります。 コード片のコピペの場合、「どのバージョンのコピーを、どのバージョンのコンパイラーにペーストするか」がわからないので、「事前に警告して、事前にコード修正をかけてもらう」戦略がやりにくいです。 (こういう問題も、top-level statemsntですでに経験済み。)