多くの場合において、特定の場所で使用可能な型はただひとつのはずだ。しかしC#では、依然として型を明示的に指定する必要がある。今回、ターゲット型(Target-typed)'new'式の提案がC# 9で採用されたことにより、このようなボイラープレートコードは不要になる。
最初の文章に見覚えがあるとすれば、それは昨年1月に、この提案を取り上げているからだ。当時、ターゲット型'new'式のプロトタイプはC# 8の一部だった。結果的には採用されなかったが、開発作業は続けられていて、今回そのステータスが"Merged into 16.7p1"となったのだ。
この機能についてすでに知っているのであれば、全体的なデザインとしては何も変わっていない。事実、構文的には、2017年にC# 7.1で検討されて以来、まったく変更されていないのだ。初めて見るのであれば、基本的にはvarキーワードの反対である。変数宣言の型名を省略するのではなく、値を生成する側での型名を省略するのだ。例を2つ挙げる。
private Dictionary<string, List<int>> field = new Dictionary<string, List<int>>();
private Dictionary<string, List<int>> field = new();
XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true });
XmlReader.Create(reader, new() { IgnoreWhitespace = true });
開発者の視点から見れば一目瞭然だろう。この機能は、冗長か、あるいは単に無意味な型宣言を不要にするものだ。しかし、言語設計の観点からは、多くの問題を考慮しなければならない。
例えば、2つのオーバーロードが可能な場合はどうするのか?コンパイラが"最適"なものを選択するべきか、あるいはoutパラメータの型のみが異なる2つのオーバーロードの場合と同じく、曖昧性エラーとしてマークすべきなのか?
LDMの記事によると、Micosoftは後者を選択した。理由のひとつは、新たなオーバーロードが追加された場合に、重大な変更が発生する可能性を低くするためだ。この"低くする"、という表現に注意してほしい。この手の推論の常として、オーバーロードの追加による問題の影響を受けやすいのだ。
言語設計における一般的な問題として、不適切なオーバーロードを除外するタイミングの決定、というものがある。これまでは、コンパイラがひとつのオーバーロードを選択して、それがジェネリックのパラメータ制約に違反しているために、後になってコンパイルエラーが発生する、というケースがあった。これは"Late Filter Approach"と呼ばれるアプローチで、コンパイラの設計が簡略化できる反面、コードの任意の部分においてコンパイラが適切なオーバーロードを検出する確率は低くなる。
これに対して"Early Filter Approach"は、可能な限り多くのオーバーロードの除外を事前に試みる方法である。この場合は、より適切なマッチングを見つけられる可能性が高まるのと引き換えに、コンパイラは複雑なものになる。LDMの記事から例を紹介しよう。
struct S1 { public int x; }
struct S2 {}
M(S1 s1);
M(S2 s2);
M(new () { x = 43 }); // ambiguous with late filter, resolved with early.
Early Filter Aproachでは、S2
がx
というフィールドを持たないことをコンパイラが検出し、可能性のある候補から除外する。Late Filter Approachを使う場合、コンパイラが決定前に見るのはコンストラクタのパラメータのみである。
想像できるように、コンストラクタがネストし始めると、Early Filterのシナリオは非常に複雑なものになる。そのため、LDMにもあるように、MicrosoftはLate Filter Approachの使用を選択したのだ。
同じLDMによれば、以下のようなシナリオは、"コンストラクト不能な(Unconstructible)型"であるため、サポートされていない。
- ポインタ型
- 配列型
- 抽象クラス
- インターフェース
- 列挙型
列挙型が対象外なのは、SomeEnum x = new()
よりもSomeEnum x = 0
やSomeEnum x = default
の方が明確なので、メリットがないためだ。抽象型はコンストラクト不能であるのは当然だが、インターフェースについては、見た目よりも実際には興味深い部分がある。
ほとんどの人たちが意識していないが、C#は、インターフェースのデフォルト実装クラスという概念をサポートしている。Xenopimate(Redditのユーザ)がその例を紹介している。
[Guid("899F54DB-5BA9-47D2-9A4D-7795719EE2F2")]
[ComImport()]
[CoClass(typeof(FooImpl))]
public interface IFoo
{
void Bar();
}
public class FooImpl : IFoo
{
public void Bar()
{
Console.WriteLine("XXXX");
/* ... */
}
}
public static void Main()
{
var foo = new IFoo(); // works just fine
foo.Bar();
}
非常に分かりづらいシナリオではあるので、ターゲット型'new'式の機能を設計するにあたって、Microsoftがこれを考慮外としたことは首肯できる。
もうひとつ疑問として浮かぶのは、"throw new()"は許容されるのか、という点だ。理屈では基本のExceptionをスローすればよいはずだが、Exceptionをスローするのは悪いプラクティスと見なされているため、この構文はサポートされていない。