Visual Studio 16.11 Preview 2 が来ていて、これに C# 10.0 の新機能が2つほど merge されています。 (いつも通り、LangVersion preview を入れれば利用可能になっています。)

ちなみに本当は 16.10 Preview 3 のときに sealed record ToString って機能もひっそりと入ってるんですが、 まあ下手すると誰も気づかないレベルの修正なので説明省略… (先月全然ブログを書いてないことへの言い訳。)

record struct

はい。レコード型値型(構造体)でも作れるようになりました。 C# 9.0 時点で、単に record キーワードを使って型定義すると必ず参照型(クラス)になっていたんですが、C# 10.0 では record structrecord class で値型・参照型を選べるようになりました。

// こっちは構造体なのでヒープ アロケーション起きない。
// あんまりでかいデータを持たせるとコピーのコストが結構でかい。
var s = new S(1, 2);

// こっちはクラスなのでアロケーション発生。
var c = new C(1, 2);

record struct S(int X, int Y);
record class C(int X, int Y);

ちなみに、単なる record はこれまで通りクラスです。 recordrecord class は完全に同じ意味。

struct と record struct

レコード型は元々「構造体的な扱いができる参照型」でした。 構造体みたいに、メンバーごとのクローン、メンバーごとの値比較ができるクラスみたいなものです。

じゃあ、record struct は普通の struct と何が違うかと言うと、以下のような点。

  • プライマリ コンストラクターを持てる
  • プライマリ コンストラクターの引数からプロパティが自動生成される
  • 以下のメソッドが自動的に作られる
    • Deconstruct メソッド
    • ToString
    • Equals, GetHashCode (IEqualtable<T> インターフェイスの実装)
    • ==, != 演算子

struct と with

あと、今回一緒に、普通の構造体に対しても withが使えるようになっています。

var s1 = new S { X = 1, Y = 2 };
var s2 = s1 with { X = 3 };

Console.WriteLine(s2); // (3, 2)

struct S
{
    public int X { get; init; }
    public int Y { get; init; }
    public override string ToString() => (X, Y).ToString();
}

構造体では、ある変数から別の変数に代入したとき、元から自動的にコピーを作っていたので、それをそのまま使っています。

global using

global using を使うと、プロジェクト全体に対して有効な using ディレクティブを書けます。

例えば、ある1ファイルに以下のようなコードを書いたとします。

global using static System.Console;
global using System.Linq;
global using System.Collections.Generic;

そのプロジェクト内では、以下のようなコードが普通に書けます。

var x = new List<int> { 1, 2, 3 };
var y = x.Select(i => i * i);
foreach (var i in y) WriteLine(i);

トップ レベル ステートメントと合わせると、本当にこの3行だけで「コンパイルできて実行できるコード」になります。 「ネットで見かけたサンプル コードをコピペしたら動かない」というクレームが減るかと思われます。 (これが一番のメリット。)

あと、「DateOnly なんて名前嫌だーーー」という方は以下のように書いておけます。一応。(別に推奨はしない。)

global using Date = System.DateOnly;

通常 using と同列

global using は、「そのプロジェクト内のすべてのファイルの先頭に using があるのと一緒」みたいな挙動をします。 つまり、「通常 using よりも外側のスコープ」みたいなことにはなりません。 あくまで「通常 using と同列」です。

例えばどこかのファイルに以下のような System への global using があったとします。

global using System;

で、これと同じプロジェクト内で通常の using を書く場合、以下のような挙動をします。

using System; // すでに global using System; があるので「重複」警告あり

using X = DateTime; // この行はコンパイル エラー。ここでは using System; ありきにはならない。
using Y = System.DateTime; // こっちは OK

namespace A
{
    using X = DateTime; // これも OK。A の外に using System; があるので。
}

知らないところで using されてる問題

別に global かどうか以前の問題なんですが、「using しすぎ」は問題を起こすことがあります。 まず、同じ名前の型があった場合に「どっちかわからない」エラーを起こします。 単純に IDE 上での補完候補が増えすぎてうざいとかもあります。 それに、C# の場合、拡張メソッドという、using の有無で挙動が変わる機能があったりもします。

global using ではそれをプロジェクト全体にわたってできるわけですから、 嫌がらせしようと思えばいくらでも嫌がらせができます。 とりあえず名前被りの例:

// JsonSerializer クラスがどれにもあるので、フルネームで書かないと弁別不能になる。
global using Newtonsoft.Json;
global using Utf8Json;
global using System.Text.Json;

ちなみに、global using は複数のファイルに書けます。 上記嫌がらせの3行を、それぞれ全く別のファイルに書いておくということもできます。

一方で、一応、ファイルの先頭にしか書けないという縛りはあります。

using System;

class Program
{
    static void Main()
    {
        // 超絶長い Main 処理を延々と書いたりもありえなくはない
    }
}

global using System.Linq; // さすがにこの行はコンパイル エラー

問題を起こせる範囲

ただまあ、global using の影響範囲はプロジェクト内に限られるので、 嫌がらせができるとすれば基本的に「内部犯」になります。

global using で一番邪悪なことやった人が優勝」とかいうひどいタイトルで配信してアイディアを募ろうとしていたり。

それで例として「Where 拡張メソッドの乗っ取り」を挙げてはいるんですが… 拡張メソッドで悪さをしたければ、トップ レベルのクラス(名前空間なしのグローバルなクラス)に拡張メソッドを書く方がはるかにたちが悪いです。

で、内部犯であれば、レビューや単体テストをちゃんとしていればある程度は防げるはずです。 悪意を持って攻めるなら「数千行のコミットにしれっと混ぜ込む」とかも考えられますけども。

たいてい以下のような Analyzer を書いてしまえば対処できちゃいそうなんですよねぇ…

  • 複数のファイルに global using を書けなくする
  • 拡張メソッドを含む名前空間を global using できなくする
  • global using した名前空間中の型名の被りに対して警告を出す

あと、global usingSource Generator で生成することもできます。 これが唯一の「プロジェクト外に影響を及ぼせる global using」になるんですが… こちらはこちらで、「信用ならないパッケージを参照するのが怖いのは元から」ですし、 Source Generator を書ける人自体が割合そんなに多くないですし。

なんかこう、レビューをうまくすり抜けたり、「嫌な予感しかしないんだけどメリットもありそうでやむなく使う」みたいな邪悪さを出せないものかと悩み中…