Quantcast
Channel: ++C++; // 未確認飛行 C ブログ
Viewing all 483 articles
Browse latest View live

【Visual Studio】 Naming Style 設定

$
0
0

こないだの C# 配信で、 「フィールドの naming style を _camelCase にするための設定を .editorconfig で書いておきたい」という話になったやつ。

.editorconfig がらみの話になったのは 1:57:52 頃~

private/internal フィールドの名前規約

長らく C#/.NET 方面は private なところの規約についてはそこまでうるさく言われない文化だったりしたのでそこまで統一見解はないんですが、 皆様は private フィールドの名前をどうしていますでしょうか。

最近では、 dotnet/runtime_ 開始の camelCase を採用したということで、このルールを支持する人が増えたというか、 this.x 派だった人も「dotnet/runtime がそういうのなら」という感じでちらほら改宗していたりはします。

class C
{
    private DateTime _date;
}

ところで、以下のスクショをご覧ください。 (フィールドに対する名前の提案。)

Visual Studio が提案してくる名前(元)

Visual Studio を触っている人なら1度は思ったことがあると思うんですが、 「あっ、そこは _ 付けてくれないんだ…」

Naming Style 設定

ということで、okazuki さん曰く、 ちゃんと _ 始まりで提案してもらえるように設定を入れているとのこと。

.editorconfig に以下のような行を入れておくと _ 始まりになります。

[*.{cs,vb}]

dotnet_naming_rule.private_or_internal_field_should_be_begin_with__.severity = suggestion
dotnet_naming_rule.private_or_internal_field_should_be_begin_with__.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_begin_with__.style = begin_with__

dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private
dotnet_naming_symbols.private_or_internal_field.required_modifiers = 

dotnet_naming_style.begin_with__.required_prefix = _
dotnet_naming_style.begin_with__.required_suffix = 
dotnet_naming_style.begin_with__.word_separator = 
dotnet_naming_style.begin_with__.capitalization = camel_case

(style, symbols, rule の3つ組が必要みたいです。)

この状態で先ほどと同じ変数名の提案を出すと以下のように変化します。

Visual Studio が提案してくる名前(設定追加後)

ちなみに、こんな構文&変数名、覚えられるわけもなく、okazuki さんは Visual Studio 上のオプション画面でこの設定を入れて、.editorconfig にエクスポートして使っていたそうです。

そこに、Visual Studio 17.1 Preview 2 の .editorconfig の GUI 設定画面に Naming Style のタブが増えたということで期待しているという状態。 (といっても、 .editorconfig GUI では "begin_with__" みたいな新規スタイル追加はできないっぽくてまだまだいまいちな感じ。)


MemberNotNull (値型) 判定

$
0
0

こないだ、[null フロー解析]と似たノリで、構造体の default フロー解析が必要という話をしました。

まあ、難航しそうではあるんですが…

とはいえ実は現在でも、「null チェックといいつつ、構造体に対しても働くフロー解析」があったりします。

MemberNotNull

nullable enable のとき、 非 null 参照型のフィールドやプロパティは、 コンストラクター内でちゃんと初期化する必要があります。

例えば以下のコードはプロパティ定義の行に警告。

class C
{
    public string S { get; } // CS8618 警告
}

以下のようにコンストラクターを足すと、今度はコンストラクターの行に警告。

class C
{
    public string S { get; }
    public C() { } // CS8618 警告
}

以下のように書くと警告は消えるんですが、

class C
{
    public string S { get; }
    public C()
    {
        S = "値は適当"; // これで警告が消える。
    }
}

これをメソッド抽出してしまうと再び警告が出ます。

class C
{
    public string S { get; private set; }

    public C() // 再び CS8618
    {
        Initialize();
    }

    private void Initialize()
    {
        S = "値は適当";
    }
}

null 許容参照型の初期リリースではこの問題を回避する手段はなかったんですが、後々、MemberNotNull という属性が追加されていて、 以下のように書けば警告をなくすことができるようになりました。

using System.Diagnostics.CodeAnalysis;

class C
{
    public string S { get; private set; }

    public C()
    {
        Initialize();
    }

    [MemberNotNull(nameof(S))]
    private void Initialize()
    {
        S = "値は適当"; // 逆に、この行を消すと CS8774 警告。
    }
}

値型に対して MemberNotNull

そしてここでようやく本題。

MemberNotNull なんて名前をしていますが、 実際には「値を代入したかどうか」を見ているようで、 値型に対しても使えたりします。

using System.Diagnostics.CodeAnalysis;

class C
{
    public DateOnly D { get; private set; }
    public C() = > Initialize();

    [MemberNotNull(nameof(D))]
    private void Initialize()
    {
    } // CS8774
    // member not "null" と言いつつ、非 null が確定している値型に対してもフロー解析してる。
}

「代入したかどうか」しか調べてない雰囲気?

代入さえされていれば D = default; でも警告が消えたりします。 (C# 10.0 時点では。)

using System.Diagnostics.CodeAnalysis;

class C
{
    public DateOnly D { get; private set; }
    public C() => Initialize();

    [MemberNotNull(nameof(D))]
    private void Initialize()
    {
        D = default; // これでも OK。
    }
}

ということで、defaultable value type の仕様が入るまではまだ機能不足ではあるんですが。 とりあえず、MemberNotNull に対して値型のプロパティを渡せなくするみたいな処理をあえて入れたりはしていないようです。 将来的に defaultable value type のフロー解析もあるだろう見込みがあるのではじかないようにしてあるんじゃないかなぁと思います。

【C# 11 候補】 トップ レベル ステートメントの Main に属性を付ける

$
0
0

ちょっと体調崩し気味だったので軽いネタに逃げる感じでわかりやすい C# 11 候補を1つ。

トップ レベル ステートメント(が作る Main メソッド)に属性を付けたいという話があります。

もう、割かし以下の利用例1個で説明終わりな感じ。

[main: STAThread]

using System.Windows;

Clipboard.SetData(DataFormats.Text, Environment.OSVersion.ToString());

今、これと同じことをしようと思ったら、これだけのために class Program { static void Main() { } } が必要です。

とはいえ、Main メソッドに付けたい属性って STAThread 以外に何かありますかね?

という意味でニッチな需要ではあるんですけど、まあ、実装コストも低そうなので割かしやる気みたいです。

nullable 警告もみ消し(来年までの我慢)の手段

$
0
0

今日はとあるアンケートの結果を乗せておこう的な話。

背景: 非 null プロパティの初期化

null 許容参照型の仕様が入って以来、以下のようなコードに警告が出るようになりました。

class A
{
    public string X;
    public string Y { get; set; }
    public string Z { get; init; }
}

C# 10.0 現在、この警告を回避する唯一の方法は「ちゃんとコンストラクターで初期化すること」です。

class A
{
    public string X;
    public string Y { get; set; }
    public string Z { get; init; }

    public A(string x, string y, string z) => (X, Y, Z) = (x, y, z);
}

困るのが、「このプロパティは new() { X = "", Y = "", Z = "" } みたいに初期化子で初期化したい」という場面。 結構あると思うんですよね、コンストラクターを定義したくない・できないとき。 今のところいい解決策がない状態です。

【将来予定】 required

一応補足。 将来的には解消する予定です。 今のところ C# 11.0 目標で、required 修飾子を付けるという案が進められています。

class A
{
    public required string X;
    public required string Y { get; set; }
    public required string Z { get; init; }
}

これが付いていると、オブジェクト初期化子で非 null な値を渡すことを義務付けられるようになるので、クラス定義側には警告が出なくなります。

var a1 = new A(); // required プロパティ/フィールドに値を与えていないので警告

var a2 = new A()
{
    X = null, // null を与えたので警告
};

// required プロパティ/フィールド全てにちゃんと値を与えたのでOKに
var a3 = new A()
{
    X = "",
    Y = "",
    Z = "",
};

現状の回避策

ということで、required によって来年には根本解決の当てがあるわけですが。 そうなると、今現在 A の作者が頑張って回避策を取る必要もないよなぁ… ということになって、 「来年まではやっつけ対処でもみ消ししとこう」という発想になります。

ただ、 こういうやっつけほど具体的にどう対処しようか迷います。 また、もみ消しにいくつかの手段があるのでその点もちょっと迷うポイント。

ということでアンケート。

選択肢

選択肢1. 該当行を nullable disable

class A
{
#nullable disable warnings
    public string X;
    public string Y { get; set; }
    public string Z { get; init; }
#nullable restore warnings
}

選択肢2. 該当行を pragma warning disable

class A
{
#pragma warning disable CS8618
    public string X;
    public string Y { get; set; }
    public string Z { get; init; }
#pragma warning restore CS8618
}

選択肢3. とりあえず default!null! を代入

class A
{
    public string X = null!;
    public string Y { get; set; } = null!;
    public string Z { get; init; } = null!;
}

選択肢4. ノーガード。警告出っぱなしなのをあきらめる

class A
{
    public string X;
    public string Y { get; set; }
    public string Z { get; init; }
}

結果

C# 配信中にこの話題になり、 配信真っ最中に Twitter アンケートを作って投票してもらったり。

https://twitter.com/ufcpp/status/1434168597060853760

! でもみ消し派が35%くらいでちょっと多めですね。 まあ思ったよりは差が広がらず。

【C# 11 候補】 ReadOnlySpan 最適化

$
0
0

dotnet/runtimeのコミット履歴とかにうっすら痕跡が見て取れるんですが、 去年の10月中旬頃、 「low level hackathon」とかいう Microsoft 社内イベントをやっていたみたいです。

今、C# 7.2とかの頃に Span<T> 構造体が追加されて以来の4年ぶりくらいの動きになりますが、 .NET ランタイムの低層に手を入れてパフォーマンス改善を図りたい流れになっているみたいです。

その後の様子を見るに、これは昨年10月の hackathon 時だけの短期的なブームというわけでもなくて、割かし .NET 7 目標にちゃんと動き出している雰囲気です。

ということで、今日の分のブログから数回はこの手の low level なパフォーマンス改善系の話をしていこうかと思います。

定数配列

今日は、以下のような、 全要素が定数の配列を書いたときの最適化の話になります。

ReadOnlySpan<int> data = new[] { 1, 2, 3, 4, 5, 6, 7, 8 };

例えば以下のような2つのメソッドを比べてみましょう。

static int M1(int i)
{
    ReadOnlySpan<int> table = new[] { 1, 0, -1, 0 }; // 差はこの行だけ
    return table[i % 4];
}

static int M2(int i)
{
    ReadOnlySpan<sbyte> table = new sbyte[] { 1, 0, -1, 0 }; // 差はこの行だけ
    return table[i % 4];
}

4要素の定数テーブルを引いているだけのシンプルなコードです。 M1M2 の差はテーブルが intsbyte かという点だけになりますが、 (少なくとも .NET 6 / C# 10.0 では) この差だけでパフォーマンスが数倍違います。

ベンチマーク用コード

一番大きな差は、

  • int の方は普通に配列が new されている (newarr 命令が発行されてる)
  • sbyte の方は 生データが直接参照されて、new ReadOnlySpan(void*, int) が呼ばれている

という点になります。 その結果、配列のヒープ アロケーションが発生するかどうかでパフォーマンスに大きな差が出ます。 (int の方がだいぶ遅い。)

エンディアン

C# 10.0 な現状、この手の最適化は bytesbyte に対してしか掛からないという制限があります。

理由は主にエンディアンで、 一応、.NET ランタイムはビッグエンディアンにも対応しているので、 new[] { 1, 2, ... } と書いてバイト列としてデータを記録するとき、 0, 0, 0, 1, 0, 0, 0, 2, ... と並べるか、 1, 0, 0, 0, 2, 0, 0, 0, ... と並べるかという問題があります。

とはいえ、これは別に今までの「要素が全部定数の配列」でも同じ問題はあって、

  • DLL 中にデータが埋め込まれる場合、.NET はリトルエンディアン
  • 埋め込みデータから配列を作るときに RuntimeHelpers.InitializeArray メソッドを呼ぶ
  • InitializeArray の中で、ビッグエンディアン環境だったらエンディアンをひっくり返す処理が入っている

みたいな動作をしているみたいです。

CreateSpan

ならまあ、やるべきことは割かしわかりやすいわけでして。 埋め込みデータから直接 ReadOnlySpan を作る部分を InitializeArray と同様のヘルパー メソッドにして、 ビッグエンディアン環境だったらひっくり返す処理を挟めばいいということになります。

それがこちら:

Add non-intrinsic implementation for CreateSpan<T>.`

Roslyn (C# コンパイラー)側の対応:

RuntimeHelpers.CreateSpan optimization for stackalloc

dotnet/runtime 内で既存コードに対してこれを前提にした最適化を掛けたもの:

Experiment with Roslyn optimization for ROS<T> in assembly data section

これが正式に採用されれば、 最初に例に挙げた M1 メソッドと M2 メソッドのパフォーマンス差はもう少し縮まるはずです。

【C# 11 候補】params Span

$
0
0

今日は「low level hackathon」話2個目。

可変長配列

C# の可変長配列は、一時的にデータを詰めておく配列を作ってメソッドに渡す作りになっています。 例えば、void m(params int[] args) というメソッドがあったとして、 m(1, 2, 3); みたいに呼び出した場合、 m(new int[] { 1, 2, 3 }); みたいに展開されます。

ここで問題になるのが new int[] でヒープ アロケーションが発生する点。 あまりにも数が重なると無視できないコストになってきます。

params Span

C# 7.2 の頃に Span<T> 構造体が入ったことで、 当然 params T[]new T[] によるアロケーションも避けたいという話が出てきます。

つまるところ、

  • メソッド定義
    • 今あるもの: void m(params T[] args)
    • 欲しいもの: void m(params Span<T> args)
  • 呼び出し側の展開結果:
    • 今あるもの: m(new int[] { 1, 2, 3 }); とか
    • 欲しいもの: m(stackalloc int[] { 1, 2, 3 }); とか

みたいなものが欲しいと。

実際、案自体は結構昔からあります:

params Span<T>

参照型 stackalloc (没気味)

ただ、stackalloc の制限が結構きついので、素直に上記のような展開はできません。

わかりやすい原因は、参照型に対して stackalloc を使えない点。 以下のようなコードはコンパイル エラーになります。

Span<string> span = stackalloc string[4];

これは元々ある .NET ランタイムの制限です。

参照型に対する stackalloc を下手に認めてしまうとガベージコレクションの参照トラッキングの負担が上がって、GC 発生時のコストまで見た時トータルではかえって遅くなる可能性が高いとのこと。

この制限に対して、low level hachathon で1回、任意の型に対する stackalloc をやってみる実験をしたみたいです。

Experiment with Unsafe.StackAlloc<T>

pull request がそっ閉じされてるんで、 やっぱり上記のような stackalloc の問題が許容されなかったんですかね。

ValueArray

他に、params Span<T> に使いたいのであれば固定長配列の類でもいいわけでして。 例えば以下のようなコードで「長さ4固定の配列もどき」を作ることはできます。

using System.Runtime.InteropServices;

ValueArray4<string> buffer = default;
Span<string> span = MemoryMarshal.CreateSpan(ref buffer.X0, 4);

struct ValueArray4<T>
{
    public T X0, X1, X2, X3;
}

とはいえ、こんなコードを都度手書きはしたくないわけでして。 あと、できれば ValueArray<string, 4> みたいな感じで何らかの手段で「長さ」の情報はジェネリクス的に渡したかったりはします。

それに類するものをとりあえず実装してみたという pull request が low level hackathon で出てたりします。

[hackathon] ValueArray

「試しにやってみた」実装なのでなかなかにキモイです… 現状の .NET は「ジェネリクスに型引数代わりに整数を渡す」みたいなことができないので、 1 の代わりに object[]、2 の代わりに object[,]、3 の代わりに object[,,]、… みたいな、object 配列の次元を整数代わりに使うというすごい実装。 本来であれば ValueArray<string, 4> と書きたいところを ValueArray<string, object[,,,]> と書くことになります…

using System.Runtime.InteropServices;

ValueArray<string, object[,,,]> buffer = default;
Span<string> span = buffer.AsSpan();

これはさすがにあまりにもきもいので没気味。 代替案として、「いったん属性を付けて特殊処理しようか」みたいな話になっています。

こちらだと、いちいち構造体の定義が要るみたいです。

using System.Runtime.InteropServices;

ValueArray4<string> buffer = default;
Span<string> span = buffer.AsSpan();

// この属性を付けた構造体は T 4つ分のメモリを確保する。
[InlineArray(Length = 4)]
struct ValueArray4<T>
{
    private T _element0;
    public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _element0, 4);
}

やっぱ、根本的にはジェネリクスに整数を渡せるようにしてほしいところですけどね… それは結構型システムに手を入れないといけないみたいでちょっと大変みたいです。

【C# 11 候補】 ref 型引数

$
0
0

今日は「low level」系統の話3個目。

ref 構造体の制限

今日もさかのぼること C# 7.2 の頃、Span<T> 構造体が入ったときの話から。

Span<T> 構造体は内部に ref フィールド的なものを持っていて、 変なところ(例えばもう解放したあとの不正な場所)を参照したりしないよう、ヒープ上にコピーできないという制限が掛かっています。 (詳しくはref 構造体で説明しています。)

その制限が守られているかどうかはコンパイラーがちゃんとチェックしているので安全に使えます。 ただ、C# 7.2 時点ではコンパイラーのチェックがまだ貧弱で、 過剰防衛気味になっています。 すなわち、コンパイラーや .NET ランタイムがもう少し頑張れば、 ref 構造体に掛かっている制限は多少緩めることができます。

その「過剰防衛」のうちで深刻なのが以下の2つ。

  • ジェネリクスの型引数に使えない
  • インターフェイスを実装できない

デリゲートの型引数に ref T

で、昨年10月にあった low level hachathon でその制限を緩めるプロトタイプ実装の1つとして、以下のような pull request が出ていました。

デリゲートはフィールドを一切何も持っていないことがわかっているので、 型引数に ref 構造体を与えても実は問題を起こさないだろうことがわかっています。

そして .NET の型システムは C# のレベルの型システムよりも元からチェックが甘いので、元から ref 構造体や ref T を型引数として与えることができるようです。

ということで、デリゲートに対して ref T を型引数として与えられるようにしたのが上記 pull request。 以下のようなコードが書けるようになります。

Func<ref int, ref int> f = (ref int x) => ref x;

デリゲートだけ特別扱いと言うのがちょっと気持ち悪くはあるんですが…

C# 10.0 のデリゲートの自然の型の仕様も十分気持ち悪いですからね… 「ActionFunc で表現できないものは匿名の型を作る」みたいなことをしていて、これはこれで微妙です。

// C# 10.0 時点では Func<ref int, ref int> というデリゲートは作れないので、
// しょうがないので delegate ref int Anonymous(ref int x) という匿名のデリゲート型を作ってる。
var f = (ref int x) => ref x;

int x = 10;
ref var y = ref f(ref x);

ジェネリック型引数に ref 構造体

デリゲートだけ特別扱いではやっぱりまだ制限が厳しすぎるわけですが。

具体的には、以下のような事をしたいという需要が結構高いです。

  • Span<Span<T>> みたいな、入れ子 Span を作りたい
  • Span<T>ISpanFormattable インターフェイスを実装させたい

ということで、これを認めるべく、提案ドキュメントが上がっています。

これで、例えば以下のようなコードを書けるようになります。

class Writer
{
    void Write<T>(T value)
        where T : ref struct, ISpanFormattable
    {
        Span<char> buffer = stackalloc char[100];

        // Constrained interface call which does not box
        if (value.TryFormat(buffer, out var written, default, null))
        {
            this.Write(buffer);
        }
    }

    void Write(ReadOnlySpan<char> data) { /* 省略 */ }
}

ここでちょっと気持ち悪い点が1つあるんですが… where T : ref struct は「アンチ制約」になっています。

普通、where で制約を掛けると「何も付けないときよりも渡せる型が減る」という状態になります。 例えば where T : class と書くと参照型しか渡せなくなります。

一方、where T : ref struct の場合は、「無制約だと渡せなかった ref 構造体を渡せるようになる」ということで、「何も付けないときよりも渡せる型が増える」ということになります。 制約が増えてるのではなく減っているので「アンチ制約」。

なので、もしかしたら where とは逆の単語、例えば allow とかを新キーワードとして追加すべきなのかもという話もあったりします。

【C# 11候補】 ref field

$
0
0

今日は「low level」関連4個目。

今日は ref フィールドとか、構造体を使ったパフォーマンス改善系の話。

昨年10月の low level hackathon で何かプロトタイプ実装があったわけじゃないですし、提案自体は2020年からあります。 ただ、昨年10月頃から本腰を入れて動き始めているみたいで、 .NET 7 / C# 11 でのリリースに向けて割かし前向きみたいです。

Span 構造体の中身

C# 7.2 の頃に入った Span<T> 構造体ですが、理屈上は以下のような構造体です。

readonly ref struct Span<T>
{
    private readonly ref T _field;
    private readonly int _length;
}

配列とか、stackalloc で確保したメモリ領域の先頭を ref で持っています。

「理屈上は」と前置きしているのは、この当時 (というか C# 10.0 現在でも)、C# には ref をフィールドに持つ機能がありません。

ちょこっと背景的な話をすると、

  • 初期プロトタイプ時点では、Span<T> の中身はポインター (unmanaged なやつ)で実装していた
  • それで劇的なパフォーマンス改善が得られることが実証された
  • その後、やっぱりポインター(ガベージ コレクションのトラッキング対象にならない)だとダメで、マネージ参照(要するにガベコレ対象にしたい)が必要という話になった
  • ref フィールドの追加は負担が大きいので、ByReference<T> という特殊な internal 構造体を用意して、それを .NET ランタイム内で特別扱いしてしのいだ

という経緯があります。

ということで、C# 10.0 / .NET 6 時点での Span<T> の中身は概ね以下のようになっています。

public readonly ref struct Span<T>
{
    private readonly ByReference<T> _pointer;
    private readonly int _length;
}

internal readonly ref struct ByReference<T>
{
    // 形式上こんな定義が入っているものの、ランタイム内で特殊処理して ref T に置き換えてる。
    private readonly IntPtr _value;

    [Intrinsic]
    public ref T Value => throw new PlatformNotSupportedException();
}

ref がらみの改善

この Span<T> を導入した当時から、 ByReference<T> の特別扱いがだいぶ「やっつけ」っぽいことは百も承知です。 「いつかは直すべきだが、差し当たって一番需要が高い Span<T> をリリースすることの方が先決」という判定です。

そしてその「いつか」が今ついに来たというのが冒頭で紹介したこの提案。 Span<T> / C# 7.2 が2017年末のことなので、実に5年ぶりの low level の機運。

この提案には複数の機能・目標が含まれていて、以下のようなものがあります。

  • ref 構造体に ref フィールドを持てるようにする
  • ByReference<T> の特別扱いをやめて、Span<T> などを普通に ref T を使った実装に置き換えれるようにする
  • ref 構造体が this 参照を ref 戻り値で返せるようにする
  • 現在認められていない new Span<T>(ref T reference, int length) みたいなものを、unsafe なしで作れるようにする
  • 固定長バッファーを unsafe なしで作れるようにする

Span<T> を先にリリースして、ref フィールドが後なので、 互換性のために、エスケープ解析が多少複雑になっている感じはありますが…

ちなみに、C# コンパイラー側だけじゃなく、 ラインタイム側の作業も結構必要になります。 以下のものがトラッキング用の issue。 今日の時点でも結構完了済み。


【C# 11 候補】 引数の null チェック

$
0
0

先日出た Visual Studio 17.1 Preview 3 で、引数 null チェックの簡素化構文が入りました。

m(null); // ArgumentNull 例外が出る。

void m(string x!!) { }

展開結果

上記の void m(string x!!) は以下のように展開されます。 (クラス名は実際には通常の C# では書けない変な名前で生成されます。)

void m(string x)
{
    Internal.ThrowIfNull(x, "x");
}

internal class Internal
{
    internal static void Throw(string paramName)
    {
        throw new ArgumentNullException(paramName);
    }

    internal static void ThrowIfNull(object argument, string paramName)
    {
        if (argument == null)
        {
            Throw(paramName);
        }
    }
}

もしかしたら、C# 11.0 リリースまでには、internal なコンパイラー生成メソッドではなくて、 標準ライブラリ中の ArgumentNullException.ThrowIfNull メソッドに置き変わったりするかもしれませんが、まあ、やってることは一緒です。

ちなみに、ThrowIfNull メソッドから Throw メソッドだけがさらに抽出されているのはその方がパフォーマンスがいいからです。 throw ステートメントがあるとインライン展開を阻害したりするので。

NRT と相補的な機能

初期提案が出たのは2019年の1月頃で、 C# 8.0 (2019年9月リリース)の null 許容参照型 (略して NRT)と同時期に検討されていたものです。

下手に NRT と同時期だったので紛らわしいのは紛らわしいんですが、

NRT 引数!!
コンパイル時のチェック(警告) 実行時のチェック(例外)
コンパイル結果には全く影響を及ぼさない コンパイル結果が変わる
メタデータに残る(外から見える・区別がある) 残らない(あくまで内部実装の簡単化)

という感じで、相補的な機能です。

文法案

文法的にはちょっと悩ましいんですが… 例えば、

  • 使う記号は何がいいか
    • 初期案では ! だった
    • 他に ???? throw?! みたいな話は出てはいる
    • 結局現在は !!
  • 場所
    • 内部実装にしか影響ないのにメソッド宣言部分にあっていいのか
    • void m(string x) { x!! } みたいにメソッドの中になくていいのか
    • NRT に合わせるなら string? に倣って string! x とかにしなくていいのか
  • 変に記号にせず、汎用的な contract として、requires x is not null とか書けないか
  • <Nullable>throw</Nullable> みたいなオプションで、問答無用で全ての引数に実行時 null チェックを挟めないか
  • [DisallowNull] みたいな属性ではどうか

とかさんざん言われています。

これに対して、

  • !null 抑止と同じ記号なのがまずい
    • null 抑止は「何もせず警告を無視」、!! は「実行時に例外」で大きな差
  • ?? は、null 合体との弁別が構文解析する上で大変
  • ?! も条件演算子 + 単項前置き !
  • ?? throw は単純に長い
  • string! は NRT との混同しててちょっとずれてる。NRT はあくまでコンパイル時チェックだけしたい
  • void m(string x) { x!! }x を2度書くのが結局つらい
  • 汎用 contract (requres) は、NRT 以前から何度か案が出て、プロトタイプ実装もされては何度も没ってる
  • 全ての引数に実行時 null チェックを挟むのはパフォーマンス的に論外。コンパイル オプションで throw の有無が変わるのもあまり好ましくない
  • 属性が実行時の挙動に大きな影響を及ぼすのはあんまり本望ではない

等々ありまして、結局は void m(string x!!) 案で実装されました。

再燃

で、!! の実装がリリースされたことで、 先日、dotnet/runtime 内のコードにそれを適用する pull request が出たわけですが。

約1200ファイル、2万行の差分。

そしてこれが Twitter 上で話題になり、今更ながら反発の声多数。

元々色々もめ気味の機能ですからねぇ。 まして、急に Twitter 上で話題になったことで、 普段 csharplang にいない人が急にわらわらと出てくる事態になり。 2019年にさんざんやった話を蒸し返し中…

ほんと、普段こんなに人いないのに… みんなぬるぽが好きすぎ…

C# 中の埋め込み言語

$
0
0

さかのぼること4年前、C# 中に正規表現な文字列を書くと以下のように構文ハイライトされるようになりました。

lang=regex

色が付く以外に、コード補完や構文ミスに対する警告とかも出ます。

今日はこの手の「C# 中への別言語の埋め込み」がらみの話です。

先日、4件くらい low level imprevements のブログを書いて、その中で「実に5年ぶりの low level の機運」とか書きましたが、 「埋め込み言語」にも4年ぶりの機運が来ています。

JSON

4年ぶりに何が起きたかというと、JSON にも構文ハイライトが働くようになりました。

そして Visual Studio 17.2 Preview 1 にこのコードが入っていりました。 以下スクショ。

lang=json

「JSON に対応した」というだけだと、 正直「なんで今更…」という感想が多少します。 外部の JSON ファイルを読む機会は近年どんどん増えていますが、 C# 中に JSON を埋め込む需要ってそんなにあったっけ?という感じ。

まあ、今後、対応する「埋め込み言語」を増やすか、 任意の開発者が任意の「埋め込み言語」を増やせるようにする予兆かも? という淡い期待をしつつ今後の動きを待ちましょう…

StringSyntax 属性

もう1個新しいのが、StringSyntax という属性が増えました。

これまで、「正規表現扱いされる文字列」の判定は結構特殊なことをやっていました。

  • Regex クラス関連のメソッドで、pattern という名前の引数に渡している
  • 直前に lang=regex みたいな文字列が入ったコメントが書かれている

みたいな判定をしています。

ちなみに、この2個目の仕様があるので、以下のようなコードにも構文ハイライトが掛かります。

//lang=regex
var regex = @"(?<name>\w+?\d{3}).txt";

//lang=json
var json = @"{ 'value': 123 }";

ほぼ「隠し仕様」みたいになってますが。

で、まあ、あんまりこういう特殊対応するのも微妙なので、 このたび、埋め込み言語指定用の属性ができました。 それが StringSyntax 属性。 Regex クラスのコンストラクターや Match メソッドにもこの属性が追加されています。

public class Regex
{
    public Regex([StringSyntax(StringSyntaxAttribute.Regex, "options")] string pattern) { }
}

raw string literal

今このタイミングでなのは、 raw string literal が入ったからでしょうね。 C# の文字列リテラル中にコードを埋め込みやすくなったので。 (さらに大本をたどると、raw string literal が入るきっかけは source generator です。 コード生成しだすと raw string が欲しくなるので。)

Visual Studio 17.2 Preview 1 で、以下のようなコードが書けるようになりました。 (C# 11 候補。現状は LangVersion preview が必要。)

const string regex = @"(?<name>\w+?\d{3}).txt";
var json = @"{ 'value': 123 }";

var raw = $$"""
class A
{
    public const string R = @"{{regex}}"
    public const string J = @"{{json}}"
}
""";

EmbeddedLanguages

この辺りの「埋め込み言語」の実装は Roslyn リポジトリ内の以下の場所にあります。

現状、正規表現と JSON の他に2つほど実装があります。

  • DateAndTime: DateTime などの ToString 時に使う書式。yyyy みたいなやつを補完してくれます(補間のみ)。
  • StackFrame: スタック トレース情報のハイライト。たぶん他の用途にコードを流用しただけで、今回説明しているような文字列中のハイライトには使われてなさげ。

プラグイン式

まあここまで話しておいてあれですが、 今日書いた内容、結構気持ち悪いと感じる人も多いんじゃないでしょうか。 コンパイラーの中に別言語を埋め込むとかリスクも非常に高く。

一度コンパイラーに組み込んじゃったものはめったなことでは消せないですからね。 例えばまあ、「XML リテラルを書けるようにしたら XML 自体が廃れた」みたいなこともあるわけでして。 (Visual Basic であった実話。)

そんなリスク、C# チームはよくわかっているので、 今日話したような「埋め込み言語」はコンパイラー内のコードではなく、 MEF プラグインとして外から渡す作りになっています。

素の Roslyn コンパイラー(Microsoft.CodeAnalysis.CSharp パッケージ)だけを参照していると正規表現や JSON の構文ハイライト(Classifier.GetClassifiedSpansAsync)は得られません。

前述の EmbeddedLanguages にある埋め込み言語を有効にしたければ、 Microsoft.CodeAnalysis.CSharp.Features パッケージを参照する必要があります。 このパッケージ内の MEF Export として埋め込み言語が入っていて、 それをコンパイラーが読み込むことで始めて構文ハイライトが掛かります。

という一連の流れのデモ:

この辺りの仕組み、まだこなれていないから public にしたくないので、4年間ずっと internal だったりします。 public になってくれればも、誰もが埋め込み言語を作れるようになりそうなんですけどね。 raw string literal とか source generator とかがこなれてきたくらいにそうなることを期待しています。

【C# 10.0 変更点】 構造体のフィールド初期化子にはコンストラクター必須

$
0
0

先日 Visual Studio 17.1.0 (正式リリース)と 17.2 Preview 1 が出たわけですが。

これをインストールすると、ちょこっと C# 10.0 の構造体のフィールド初期化子の挙動が変わります。 以下のようなコード、17.0/17.1 Preview 時代はコンパイルできていたんですが、17.1/17.2 Preview ではコンパイル エラーになります。

struct S
{
    public int X = 1; // ここが原因。
}

ちなみに、C# の言語バージョンが改まったわけではなく、 バグ修正とかと同じノリでサイレント修正です。

仕様: Never synthesize parameterless struct constructor

問題

上記のコードの 17.0/17.1 Preview 時代の挙動なんですが、 まあ、暗黙的に引数なしコンストラクターが追加されています。 以下のような挙動。

Console.WriteLine(new S().X); // 1

struct S
{
    public int X = 1;
    // public S() { } これがある時と同じ挙動になってた。
}

問題は、このコードに引数ありコンストラクターを足したとき。 以下のようになっていたそうです。

Console.WriteLine(new S().X); // 0。 default(S).X 扱い…

struct S
{
    public int X = 1;
    public S(int x) => X = x;
    // public S() { } これが生成されなくなる。
}

この挙動が罠すぎるので、傷が浅いうちに不具合扱いして挙動を変えようということになりました。

案1: 現状維持

もちろん、現状維持も検討されたみたいなんですが、 C# 10.0 リリース後のユーザーの反応的には相当に強い懸念の声が出ていて、無視はできないレベルと判断されたそうです。

案2: 常に引数なしコンストラクターを生成する

以下のように直すのが自然な気がしなくもないわけですが…

Console.WriteLine(new S().X); // ちゃんと1になればいいわけで。

struct S
{
    public int X = 1;
    public S(int x) => X = x;
    // public S() { } これが生成されればいい。
}

これで問題になるのが、record structプライマリ コンストラクターだそうで。

プライマリ コンストラクターがある場合、「全てのコンストラクターは最終的にプライマリ コンストラクターにたどり着く必要がある」ということになっています。

record struct S(int X)
{
    // 必ず S(int X) にたどり着くように書かないとダメ。
    public S() : this(1) { }
    public S(int a, int b) : this(a * b) { }
}

ここで、じゃあ、先ほどの、フィールド初期化子があるときにどうするか。 コンパイラーが自動的に引数なしコンストラクターを追加するのであれば、プライマリ コンストラクターには何を渡すべきかという問題がでます。

record struct S(string X)
{
    public int Y = 1;

    // public S() : this(null) { } を足す?
    // 非 null が期待される string に null が渡ってしまう…
}

これがあるから、当初、「引数ありコンストラクターがあるときにはむやみに引数なしコンストラクターを追加しない」という判断になったようです。

案3: コンストラクターが1つもないとき、フィールド初期化子をエラーに

ということで、今日のブログの冒頭の話に戻ります。

以下のコードがエラーになりました。

struct S
{
    public int X = 1;
}

ちなみに、Visual Studio 17.2 Preview 1 では、この状態の(エラーのある)コードに対して「引数なしコンストラクターを追加する」というリファクタリング機能が追加されています。

Generate constructor リファクタリング

ただ、最初から以下のようなコードを書くと罠っぽい挙動になるのは今と同じ。

Console.WriteLine(new S().X); // 0。 default(S).X 扱い…

struct S
{
    public int X = 1;
    public S(int x) => X = x;
    // public S() { } これは生成されない。
}

ただ、「後から迂闊に引数ありコンストラクターを足してしまう」という状況は減るはずです。

エラーにならないようにするのは元々が以下のようなコードのはずで、

struct S
{
    public int X = 1;
    public S() { }
}

ここに引数ありコンストラクターを足すはずなので、 以下のような挙動が期待されます。

Console.WriteLine(new S().X); // ちゃんと1。

struct S
{
    public int X = 1;
    public S() { }
    public S(int x) => X = x;
}

引数 null チェックの !!、取りやめ

$
0
0

!! を使った引数の null チェック、なくなるって。

引数 null チェック

2月にブログに書きましたが、 Visual Studio 17.1 Preview 3の頃、C# 11 候補として「引数の null チェック」構文が入っていました。

m(null); // ArgumentNull 例外が出る。

void m(string x!!) { }

今現在(VS 17.2 Preview 5)でもこの構文は生きているんですが、次(たぶん、17.2正式リリースでも17.3 Preview 1でも)でいったん取りやめになるそうです。

取りやめの経緯

C# チームとしては、今、Preview リリースをしてみて反応を見てその後どうするかを決めたりしているわけですが。 LangVersion preview があるのはそのためです。

とはいえ、普通に考えて、Preview 機能まで追いかけている人がそんなに多いわけもなく、 正式リリースされるまでどんな機能が追加されているのか知らない人の方が多数派でしょうね。 ところが今回は大変目立つ Pull Request が1個ありまして。

  • [1,232ファイル、+4,540行、-21,372行の Pull Request]

こういうので急に話題になると、大体燃えます。 以下のような質問 Discussion が立ったんですが、コメントが荒れる荒れる。

というか、null チェックがらみはいつも荒れるんですが…

この荒れ具合を受けて、4月6日に再検討:

この日の検討では、

  • やるべきじゃない?
    • → やる価値はあると信じてる
  • もっと完全な契約プログラミングにする?
    • → null チェック以外の価値は微小
  • NRT で自動的に実行時 null チェックも入れる?
    • → 破壊的変更(急に例外出るようになる)の度合いが大きすぎるし、無条件の null チェック挿入の実行時コストはちょっと許容しかねる
  • 構文再考: !! 以外を考える?
    • ! はない。かといって他の案(T parameter not null みたいなの)は長ったらしかったりできつい

みたいな話に。 で、次の4月13日に!! 以外の構文について色々検討。

とはいえ、しっくりこなかったみたいで「C# 11からは取り下げて、後日改めて検討する」ということに決定。

そして .NET Runtime 側、 !!構文をやめて、ArgumentNullException.ThrowIfNullに書き換える Pull Request が改めて出ました。

荒れた理由

いくつか私見。

突然の登場

一般的な C# 開発者に取って唐突に出て来た機能ではあったと思います。

前述の「1,232ファイル Pull Request」を見ての通り、 .NET Runtime チームにとっては結構強く求められる機能で、 要するに「内需」です。 内需の場合、割かしあっという間に C# チームに需要が伝わって、あっという間に実装されてリリースにこぎつけたりするので。

「外部開発者が知らないとこで“中の人”で勝手に話進めやがって」感があって、「これだからお前のとこの会社は」みたいに思われたんでしょうね。

マイノリティのための構文

null チェック機能は「.NET Runtime みたいに、大規模に使われているライブラリ作者にとっては有用なものの、ライブラリを使う側にとっては大して必要のない機能」です。

よっぽどかっちりライブラリを書くのでなければ、 「引数の null チェックして自分で throw new ArgumentNullException() する必要あるの?」 「自分でチェックしなくてもどうせすぐに NullReferenceException 出るし、大差ないじゃない?」 「どうせ catch もせず、デバッガーで例外が出たところに飛んで直すだけだし」 とか思いますし。

なので、「なんでそんな必要のない機能を先に実装するの?」みたいな反発を買っていそうな雰囲気があります。 (ライブラリは作る側よりも使う側の人の方が圧倒的多数なので、人口ベースで見るとマイノリティのための機能になります。)

null 理想論

この機能は「不完全な NRT のしりぬぐい」感もあるんですよね。

NRT のフロー解析が完璧ならそもそもとして「null であってはいけないところに null が渡ること自体がない」はずで、 だったら「null チェック」も「ArgumentNullException」も本来起こりえないので不要なものなわけで。

じゃあどこから null が漏れてくるかというと、

  • NRT が不完全で、構造体の default とかで簡単に「null チェックをすり抜けてくる null」がいる
  • 他の言語との相互運用、NRT 導入前の古い C# のコードなど、解析しようがないところから来る null がいる

という辺り。 この辺りのしりぬぐいとして、「フロー解析に加えて、実行時チェックして throw new ArgumentNullException()」とか、ある種の冗長な処理をやっているわけです。

結果、NRT に対する理想を抱く人ほど、!! を気持ちが悪がってる感じがあります。

個人的な意見

大多数の人間にとってはどっちでもよくて、 .NET Runtime チームにとってはそれなりに必要なんだから、 多少みっともない構文でも入れちゃっていいと思うんですけどね。

とはいえ、構文がみっともない以外にも多少、はまりそうなポイントがあったりはします。 例えば、record が絡むとどうなの?とか。 以下のようなコードの挙動を見ると、構文の問題以上にもうちょっと見当が必要かもなー、とかは思います。

// これは例外を出してもらえる。
var r1 = new R(null);

// でもこれは例外が出ない。
// init アクセッサーにも同種の null チェックを備えられるようにすべきではないか?
var r2 = new R("") { X = null };

record R(string X!!);

C# でキーワードをできるだけ多く並べる遊び

$
0
0

以下のコード、有効な(エラーなくコンパイルできる) C# コードの一部です。

青いなぁ

きっかけ

Twitter でこんなのを見かけて。

雑に翻訳:

有効な C# プログラムで1行に16キーワード並べられる?少なくともそのうち半分は異なるキーワードとして。

その後の返信から、

  • 連続したキーワードのみ(< とかの記号が間に挟まってるのはダメ)
  • 文脈キーワードはあり

とのこと。

書いたコード

試しに色々考えてみたところ、「半分は異なる」どころか、「全部異なる」でも20個超えれることが判明。

Gist に全体像:

キーワードが連続しているのは以下の部分。

とりあえず重複を許容して62個、44種並べられたもの:

in await value is not bool or char or byte or sbyte or short
or ushort or int or uint or nint or nuint or long or ulong or
float or double or decimal or string and var _ as dynamic as
object on false equals null where this orderby default ascending
orderby null descending group null by static ref readonly global

これ、多少インデントをまともに整形すると以下のようなコードです。

from x in value
join y
    in await value
        is not bool or char or byte or sbyte or short
            or ushort or int or uint or nint or nuint
            or long or ulong or float or double
            or decimal or string and var _
        as dynamic
        as object
    on false equals null
where this
orderby default ascending
orderby null descending
group null by
    static ref readonly global::System.Int32() => ref NullRef<int>()

とりあえず、「Visual Studio 上で青色か紫色になるやつはキーワードとする」という前提。 Classifier"keyword""keyword - control" を返してるやつです。

ちなみに、重複を一切認めなくても27個のキーワードを並べられました。

in await value is not bool or byte and var _ as object on false equals
null where this orderby default ascending group true by static ref readonly int

昨日、最初につぶやいた時点では20個くらいだったんですが、そこからだいぶ増えて27個に。

過程

水増し要員

重複を際限なく許すのなら、以下のように、何回でも繰り返せるものがあります。

  • x is (not)×n null
  • x is int (or int)×n
  • from x in y (where true)×n select null
  • x (as object)×n

特に not は単独でいくらでも増やせるので、1個単位で個数の調整が可能。 なので、きっかけとなったツイートの「半分は異なる」の条件を満たすために「not を増やす」という水増しが可能。

とりあえず、Kirill さんの言っていた16個程度であれば、x is not null or byte or short or int... で余裕で達成できます。 Kirill さんもこれを想定してつぶやいていたんじゃないかなぁと思います。

クエリ式

キーワード並べ放題という意味ではクエリ式が強すぎでした。 select, where, orderby, group, by 等々、クエリ式内限定の文脈キーワードがたくさんありますし、 where true みたいにキーワードだけで式を構築しやすくて。

以下のように、「object 引数で何でも受け付ける拡張メソッド」を置いておくことでさらに自由度が増します。 where null でも group default by false でも何でもありです。

static partial class Ex
{
    public static object Select(this object x, Func<object, object> f) => null;
    public static object Join(this object x, object y, Func<object, object> a, Func<object, object> b, Func<object, object, object> c) => null;
    public static object Where(this object x, Func<object, object> f) => true;
    public static object OrderBy(this object x, Func<string, object> f) => null;
    public static object OrderByDescending(this object x, Func<object, object> f) => null;
    public static object GroupBy(this object x, Func<object, object> a, Func<string, object> b) => null;
}

他の選択肢

クエリ式が強すぎることで、他の選択肢が消えます。

例えば、protected internal とか sealed override とかの選択肢が消えます。 余談として、こういう「修飾子系のキーワード」で頑張る場合、現状、 unsafe protected internal sealed override partial ref readonly int の9個が最長でした。

あと、当初はパターンマッチを中心に考えていて、 「じゃあ case とか when を使えば伸びるのでは… と思っていたものの、 ここもクエリ式を組み込めなくて没になりました(キーワード14個)。

case not null and bool or byte when true as object is var _

式の並べ方

let みたいに絶対に = が挟まってしまって途切れるものは置いておいて、クエリ式の候補には以下のようなものがあります。

先頭要素(x をキーワードにできないのでそこで連続性が途切れる):

  • from x in a
  • join x in a on b equals c

それ以降の要素:

  • where a
  • orderby a (さらに後ろに ascending または descending を付けれる)
  • group a by b

(selectgroup と競合するので没。)

join から始めて、in から後ろを使うのが最長の候補です。

from x in n join y
// ここから下がキーワード候補
in a on b equals c
where d
orderby e ascending
group f by g

(重複を許すなら orderby descending を追加。)

単独キーワード

前節のクエリ式のうち af の6個には、単独で有効な式になれるキーワードが必要です。 x is int or int... とか x as object as object... とかで水増しするにしても、 起点 x になれる物が必要なので。

候補には、

  • null, true, false: どこでも使えるリテラル
  • default: ターゲット型推論が効くとき限定で使えるリテラル
  • this: クラスのインスタンスメンバー内限定
  • value: プロパティの set 内限定
  • args: トップ レベル内限定

があって、このうち、valueargs は両立不可能。 thisargs も両立不可。 両立できなくて困る args を除いて、偶然にも、ちょうど必要な6種でした。 (C# チームはこの縛りを見抜いていた!?)

where default (ちゃんと Where メソッドの引数から型推論可能)とかが通ったのも助かりました。

あと、将来(たぶん、C# 11 で)、プロパティ内限定で使える field キーワードも追加されそうです。 (これも args と両立不可。value, this とは可能。)

末尾キーワード

前述のクエリ式のうち g については、「どうやっても後ろに記号がくっついてくるキーワード」が使えます。 例えば、以下のような候補あり。

  • new object(): object の代わりに global::System.Object とかを使えば global の巻き込みもできる
  • true with { }: with の前は構造体でないとダメなので truefalse くらいしか選択肢なし
  • static () => { }: ラムダ式

ラムダ式の案を思いつくまでは new global::System.Object() が最長だと思って使っていました。 (trueaf の方で使いたい。)

で、途中でC# 10 で導入されたラムダ式の戻り値指定が使えることに気づいて一気に伸びました。 パターン (x is int みたいなところ)には使えなくても、 ラムダ式戻り値としてなら static, ref, readonly などの修飾子が使えます。

以下のようなコードの、static ref readonly global の部分が使えました。

using static System.Runtime.CompilerServices.Unsafe;

var f = static ref readonly global::System.Int32 () => ref NullRef<int>()

末尾限定で global が使えることも分かっているので、型名は int ではなく global::System.Int32 で参照しています。

修飾

null とか value とかは、await value is null みたいにある程度前後を修飾できます。

await

await も以下のような拡張メソッドを用意しておくことで任意のオブジェクトに対して使えます。

using System.Runtime.CompilerServices;

static partial class Ex
{
    public static ValueTaskAwaiter<object> GetAwaiter(this object x) => default;
}

ただ、前述の通り、value を使いたければプロパティの set 内である必要があります。 プロパティは非同期にはできないので、1段工夫が必要で、以下のように、ラムダ式で覆う必要がありました。

public object X
{
    set => _x = async () => ...

is

Where とか OrderBy とかを object 引数で定義したので、別に bool を渡しても大丈夫です。 なので、orderby value is null (bool になっちゃう)とかも書けます。 ということで、パターン使い放題。

特に、C# 9 で not, and, orが追加されたので、これで結構伸ばせます。

重複なしなら以下のパターン。

is not bool or int and var _

not null とかも書けるんですが、null は前述の「単独で使える貴重なキーワード」なので、ここでは避けます。

また、この文脈においては _discard の意味になるので、キーワード扱い(Visual Studio 上で青色)になります。

重複を許すのであれば、or byte or sbyte or short... というように、全ての組み込み型を or でつなぐことでかさ増しできます。

当初、char, nint, nuint を忘れてました…

as

as も含めたいがために、1個だけ or object とはせずに as object で使いました。

ここで、x is dynamic とは書けないものの、x as dynamic なら書けるとご指摘いただき、 無事1キーワード増えました。

まとめ

青いなぁ。

「こんなコード書きたくないし、書いた自分でも読めない」な状態ですが、 思った以上にキーワードを大量に並べることができました。

当初は35個だったんですが、 9個増えて44個になりました。

  • char, nint, nuint 忘れ
  • orderby descending 忘れ
  • await 導入
  • as dynamic 導入
  • ラムダ式戻り値の導入

だいたいはクエリ式のせいですが、 クエリ式を使わず重複なしでも case から始まる14キーワードとかを並べられるみたいです。

色々やっているうちに、in a on b equals c where d orderby e group f by... みたいなのに必要な6種類のキーワードがピッタリあって、この縛りを見抜かれていた感があります。

.NET 7 Preview 7 で、C# 11 の機能が一通りそろったみたい

$
0
0

久々のブログになります。 C# 11 の機能追加があるたびに YouTube 配信ではちょくちょく紹介していましたが、 こっちではかなりの久々。

そういえば去年とかは新しい Preview が出るたびに「今回はこの機能が実装されたよ」一覧くらいはブログに書いてたなと思いつつ。 まあ、今年は早い段階から「C# 11.0 の新機能」の方を埋める作業をしているので、何もしてなかったわけでもないんですが。 ちなみに、「C# 11.0 の新機能」の方は現在、 進捗 12/19 です。

.NET 7 Preview 7 での C

しばらくブログとしては書いてなかった Preview 版の紹介を、今回久々に書いているのは、 .NET 7 Preview 7 で、

  • 予定されている C# 11 の機能が一通り全部入った。今ないものは RC/GA でもない
  • LangVersionpreview を指定しなくても (net7.0 ターゲットならデフォルトで) C# 11 が使えるようになった

という2点から。

去年どうでしたっけ。.NET 6 では RC 1 のタイミングで preview が外れてたような…

C# 11 最終版

とりあえず、Language Feature Status では一通り「11」の機能がそろいました。 まあ、一部、ちょっと修正が入りそうな部分はありますが、大まかな機能としてはもう変更はなさそうな雰囲気です。

「ちょっとした修正」も、例えば以下のようなバグの修正みたいなレベルの話です。

unsafe
{
    // ref フィールドを持ってる構造体、 .NET 7 Preview 7 では managed 扱いされないバグがあるみたい。
    // (ガベコレ的にまずいコード。)
    var a = stackalloc RefInt[4];
}

ref struct RefInt
{
    public ref int Reference;
}

preview 外れた

とりあえず、TargetFrameworknet7.0 なプロジェクトは LangVersion を書かなくても C# 11 になります。 あと、LangVersionlatest とかにしているプロジェクトでも C# 11 になります。

注意点として、今回、「net6.0 と C# 11」みたいな組み合わせにするとちょっと問題を起こすような破壊的変更があったりします。 以下のようなやつで、まあ、めったに踏むようなコードでもないとは思いますが、一応。

using System.Runtime.InteropServices;

public struct Buffer
{
    private int _item0;

    // net7.0 なら問題なくコンパイルできる。
    // net6.0 + C# 10 でも問題なくコンパイルできる。
    // net6.0 + C# 11 だとコンパイルエラーになるので注意。
    public Span<int> AsSpan() => MemoryMarshal.CreateSpan(ref _item0, 1);
}

Blazor Wasm 実動作デモはじめました

$
0
0

昔、うちのサイトのページ内に iframe で張り付けとくような実動作デモをいろいろと Silverlight 作ってたんですが、 Silverlight のサポート終了後、移行先がなくてほったらかしになっていました。

その時が来たら本気出す」とかいう雑なタグをつけて放置してたんですが、 そろそろ Blazor WebAssembly 化でもしてみようかという感じで数年越しに作業する気になり。

とりあえず、ソートのページで使っていたソートの可視化プログラムを移植。

移植というか、もう完全に忘れてるし、なんだったら思い立った瞬間には昔のコードをどこに置いたかわからなくなっていたので1から作ったんですが。

実物 iframe (クイックソート単品):

実物 iframe (一覧):

<span style="witdh: ...; height: ..." /> とかでバーを表示するという雑なことやっても、 スマホとかで表示しても結構ちゃんと動いていてほんと富豪的…

まあさらっとやってさらっと動いたので、 他にも何かしらこの手の実動作デモがページ内にあるとよさげなものがあれば作ろうかなという気分になっています。 (何かいいものがあれば。)


共変配列事故

$
0
0

またちょっと Gist に書き捨ててたコードが増えてきたので供養ブログをしばらく書いていこうかと。

(今年はまだ少な目。一人アドベントカレンダーな量にはならず。)

配列の共変性

悪名高いんですが、C# のというか、.NET の配列は共変だったりします。

// ↓.NET 的に許されていはいるものの、 items[0] = new Base(); が例外を起こすので今となってはあんまり使いたくない機能。
// 意図的に使うことはめったにないものの…
Base[] items = new Derived[1];

// これは問題ない
items[0] = new Derived();

// これも問題ない。 Base に Derived を代入するのは安全。
Base item = items[0];

// これがダメ。
// 実行時例外が出る。
items[0] = new Base();

class Base { }
class Derived : Base { }

実行時例外出ることわかってるんだからコンパイル時に禁止しろと… (みんな言ってる。何度でも言ってる。)

IEnumerable<T>ReadOnlySpan<T> がある現在では本当に意味不明な仕様なんですが、 まあ、 .NET の最初期(.NET Framework 1.0)の頃はジェネリクスすらなかったので、 やむなくこんな仕様を入れたんだと思います。

ちなみに、実のところ Java も配列が共変で、.NET はそれに右に倣えな感じは多少あります(初期にジェネリクスがなかったのも共通)。

事故発生

まあ、この仕様は昔の名残丸出しの気持ち悪い仕様なので、意図的に使うことはほとんどないんですが。 時々事故るんですよねぇ。

Base[] items = new Derived[1]; とかいうわかりやすいコードならやらないのであって、 型推論が絡むと時々間違っちゃう。

// 配列の型推論はソース側(右辺側)からしかやらない。
// となると…
Base[] items = new[] { new Derived() };

// 1. new[]{} の中身が Derived である
// 2. 中身からの型推論で、右辺の型は Derived[] になる
// 3. Base[] に Derive[] を代入(共変)している

// はい、アウト。実行時例外が出る。
items[0] = new Base();

class Base { }
class Derived : Base { }

数年に1度はやっちゃう…

ちなみに今年やったのはもうちょっと複雑で、要点を抜き出すと以下のようなコードでした。

var testData = new[]
{
    // たくさん new A() が並んでる。
    new A
    {
        Child = new()
        {
            Items = new[] // これの推論結果は Base[] なのでセーフ。
            {
                new Base(), new Derived(),
            },
        },
    },
    new A
    {
        Child = new()
        {
            Items = new[] // これが Derived[] になってアウト。
            {
                new Derived(), new Derived(),
            },
        },
    },
    // たくさん new A() が並んでる。
};

// いろいろあって最終的に B.Items が Deserialize に渡る。
// こっちは平気だけど…
Serializer.Deserialize(testData[0].Child!.Items!);

// こっちは実行時例外起こす。
Serializer.Deserialize(testData[1].Child!.Items!);

class Serializer
{
    public static void Deserialize<T>(T[] value)
    {
        // ちなみに、共変配列が来てるとここの Span へのキャストのタイミングで実行時例外。
        Deserialize((Span<T>)value);
    }

    public static void Deserialize<T>(Span<T> value)
    {
        foreach (ref var x in value)
        {
            // x = ...
        }
    }
}

class A { public B? Child; }
class B { public Base[]? Items; }

class Base { }
class Derived : Base { }

来年には入るかもと目されているコレクション リテラルではこんな問題起こさないように設計されていそうで。 この時ばかりはかなり本気で、一刻も早くコレクション リテラルに来てほしいと思いました。

Visual Studio の .NET Core 化まだー?

$
0
0

C# 配信でちょくちょく出てくる話題の1つに

「Visual Studio (for Windows)はいまだに .NET Framework だから」

というものがあります。 もちろん、「.NET Core 化はよ」みたいな文脈です。

Visual Studio は .NET 製アプリの中でも大規模なものの1つなわけで、ドッグフーディング的な意味で早く .NET Core 化してほしいというのもありますし。

.NET Framework → .NET 5 → .NET 6 → .NET 7 と、毎度2・3割は速くなってるというベンチマークがあるわけで合計すると2倍以上速いかもしれず、 普通にパフォーマンス上の理由でも早く .NET Core 系になってほしかったりもします。

そしてもう1個、 実は .NET Framework の方は Unicode 8.0 で止まっているという話があったり。

C# の lexer/parser は .NET ランタイム依存

C# では、空白文字とか識別子に使える文字とかの定義に Unicode の文字カテゴリーを使っています。

そして、C# コンパイラー自身が C# 製になって以来、 カテゴリー判定には普通に .NET の GetUnicodeCategory を使っています。

そして、 「Visual Studio は .NET Framework 動作」 と「.NET Framework は Unicode 8.0 止まり」 のコンボで、 Visual Studio 上でだけコンパイルできないコードが割かし簡単に書けたりします。

C# 9.0 以降に追加された letter

C# で識別子に使える文字は、まあかなり端折って言うと、いわゆる letter と言われる文字です。

で、Unicode の各バージョンで追加された文字は、 unicode.org 内の各種データ置き場DerivedAge.txt とかで調べられます。

ちょっと Unicode 8.0 から 14.0 まで1文字ずつそれっぽい letter を適当に拾って…

  • Unicode 8.0: ᏸ U+13F8, Cherokee Small Letter Ye
  • Unicode 9.0: Ɪ U+A7AE, Small Captital I
  • Unicode 10.0: ৼ U+09FC, Bengali Letter Vedic Anusvara
  • Unicode 11.0: ՠ U+0560, Armenian Small Letter Turned Ayb
  • Unicode 12.0: Ꞻ U+A7BA, Latin Capital Letter Glottal A
  • Unicode 13.0: ഄ U+0D04, Malayalam Letter Vedic Anusvara
  • Unicode 14.0: ౝ U+0C5D, Telugu Letter Nakaara Pollu

これを、こうじゃ:

// Unicode 8.0
int  = 8; // U+13F8, Cherokee Small Letter Ye

// Unicode 9.0
int  = 9; // U+A7AE, Small Captital I

// Unicode 10.0
int  = 10; // U+09FC, Bengali Letter Vedic Anusvara

// Unicode 11.0
int ՠ = 11; // U+0560, Armenian Small Letter Turned Ayb

// Unicode 12.0
int  = 12; // U+A7BA, Latin Capital Letter Glottal A

// Unicode 13.0
int  = 13; // U+0D04, Malayalam Letter Vedic Anusvara

// Unicode 14.0
int  = 14; // U+0C5D, Telugu Letter Nakaara Pollu

Console.WriteLine( +  +  + ՠ +  +  + );

(ちなみに C# コンパイラーっていまだにサロゲートペアに対応していないので、BMP 内で当該文字を探さないといけないんですが。 見ての通り、最近でも BMP への文字追加が意外とたくさんあります。)

これをエディターで開いてみましょう。

Visual Studio for Windows (左)と VS Code (右)

左が Visual Studio for Windows、右が VS Code。 .NET Framework が Unicode 8.0 で止まっている証拠の1つとなります。

ちなみに、 .NET SDK のバージョンによってもどこまでコンパイルできるか変わるはずです。 確か、 .NET 6 は Unicode 13.0 なので、 ౝ (U+0C5D、Unicode 14 での追加)はコンパイルできないと思います。

おまけ: 対 Visual Studio 専用ホモグラフ攻撃

ほんとたまたまで、 「DerivedAge.txt を眺めてて各バージョン最初に目に入った letter っぽい文字」 を選んだだけなんですが…

Ɪ と ՠ の2文字、ASCII 文字と似ててホモグラフ攻撃できそうじゃない…

class Ɪՠage { }

この Ɪՠage クラス、最初の2文字が先ほどの Ɪ (U+A7AE)と ՠ (U+0560)です。

これ、たぶん、CI とかも通っちゃうんですよね。 これがコンパイルできないのは本当に Visual Studio for Windows だけ…

raw string の空白文字

$
0
0

書き捨ててたコードの供養ブログ シリーズ。 今日は、C# 11 で入った生文字列(raw string literals)は、C# には珍しく、空白文字の数や並び順に影響を受けるという話。

C# と空白文字

C# は空白文字の影響を受けにくい言語仕様になっています。 主に2点。

  • 空白の有無によって意味が変わる場所が極めて少ない
  • 全角スペースとかが混入していても ASCII のスペースと同じ扱いをする

(C# に限らず、 C 言語の影響を受けて作られた言語で、 Unicode に対応している言語は結構こういう仕様のものが多いはず。)

空白の有無

「空白の有無」は、A BAB みたいな単語区切りを除けば、 自分の思いつく限り、意味が変わるのは x +++ y くらいでした。

var x = 1;
var y = 2;

var z = x+++y; // ここの +++

Console.WriteLine((x, y, z));

ちなみに、以下のような差。

var z1 = x++ + y; // (x++) + y
var z2 = x + ++y; // x + (++y)
var z3 = x + + +y; // x + (+(+y))
// +++ は ++ + の意味になる。

コメントまで含めると //*///* / とかも思いつきますが、 すぐに思いついたのはこれだけでした。

ASCII 以外の空白文字

C# は割かし全角スペース耐性があります。 C# は「空白かどうか」を Unicode カテゴリーを見て判定しているので、 スペースの半角・全角は問いませんし、なんだったら nbsp (HTML を書いてて時々出てくる「ここで改行しちゃだめ」スペース)とかが混入しても大丈夫です。

正確に言うと、C# の文法では以下の文字が「空白」になっています。

  • Unicode の文字カテゴリーが Zs (Space Separator)の文字
  • 水平タブ(U+0009)
  • 垂直タブ(U+000B)
  • フォーム フィード(U+000C)

ちなみに、Zs の文字は以下の通り。

  • U+0020: space
  • U+00A0: no-break space
  • U+1680: ogham space mark (オガム文字)
  • 幅違い
    • U+2000: en quad
    • U+2001: em quad
    • U+2002: en space
    • U+2003: em space
    • U+2004: three-per-em space
    • U+2005: four-per-em space
    • U+2006: six-per-em space
    • U+2007: figure space
    • U+2008: punctuation space
    • U+2009: thin space
    • U+200A: hair space
  • 幅違い no-break
    • U+202F: narrow no-break space
    • U+205F: medium matematical space
  • U+3000: ideographic space (全角スペース)

こんな文字は入力する方が大変なんですが… 頑張って入力すると、以下のようなソースコードが書けたりします。

var a = new[] {
				0,
1,
2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
    10,
    11,
    12,
    13,
    14,
    15,
    16,
    17,
    18,
    19,
};

入力も大変なら、Visual Studio みたいな IDE は自動整形機能でごっそり全部、消すか、通常のスペース(U+0020)に置き換えてくれるので、 この変なソースコードを維持するのもそれなりに大変です。

ちなみに今回は、以下のコードでコード生成しました。

using var f = new StreamWriter("a.cs");

var ws = new[] { 0x0009, 0x000B, 0x000C, 0x20, 0xA0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000 }.Select(i => (char)i).ToArray();

f.WriteLine("""
    var a = new[] {
    """);

for (int i = 0; i < ws.Length; i++)
{
    var w = ws[i];
    f.WriteLine($"{w}{w}{w}{w}{i},");
}

f.WriteLine("""
    };
    """);

raw string と空白文字

C# についてまとめると以下の通り。

  • 空白文字の種類に影響を受けることはない
  • 空白文字の有無や個数、順序に影響を受けることもほとんどない
  • Visual Studio が軒並み整形してしまうので、U+0020 (通常のスペース)以外の空白文字は維持するのも難しい

ところで、C# 11 では以下のような「複数行文字列リテラル」を書けるようになりました。

var raw = """
    raw string literals (生文字列リテラル)
    | ← ここよりも左側にあるインデントは無視される。
    ここまでがリテラル。
    """; // この「閉じ引用符」行のインデントが基準。

Console.WriteLine(raw); // 「raw」から始まる。「    raw」にはならない。末尾も改行は入らない。

ここでちょっと好奇心を働かせます。 空白文字を混在させたときの扱いはどうなるんだろう?

試してみると、コンパイル エラーでした。 CS9003「閉じ行と異なる空白を含んでいます」エラー。

_ = """
    全角スペース4つ。
    """; // スペース4つ。
_ = """
    4つ中1個だけ全角。
    """; // スペース4つ。

おっ?異なる文字がダメということは? もしや?…

_ = """
  	  全角、半角、タブ、半角、全角。
  	  """; // 全角、半角、タブ、半角、全角。

混在していても、順序を含めて完全に一致していればコンパイルできるみたいです。

じゃあ、こんな感じで…

using var f = new StreamWriter("a.cs");

var ws = new[] { 0x0009, 0x000B, 0x000C, 0x20, 0xA0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000 }.Select(i => (char)i).ToArray();
var w = string.Join("", ws);

f.WriteLine($""""
    _ = """
    {w}abc
    {w}""";
    """");

どうかな?

_ = """
	                 abc
	                 """;

コンパイルできる!

そして、どうも現状 (Visual Studio 17.5 時点) で、 この raw string のインデント部分の自動整形はしないみたいです。 (下手に整形するとさっきの CS9003 エラーの原因になるので。)

まあ、こんなコード入力するの自体困難なコードで問題を起こす愉快犯もそうそういないと思いますが。

C# らしからぬ、

  • 空白の順序に意味がある
  • Visual Studio に自動整形されない

というのが珍しかったという話になります。

余談: 謎の記号

ところで、先ほどのコード、Visual Studio で開くとこんな感じ:

謎の♂♀

なぜか♂♀記号が…

気になっていろいろ試してみたところ、 制御文字全般謎の記号に置き換わりました。 0~0x20 までの文字を書き込んで Visual Studio で開くと以下のような感じ。

0~0x20

以下のような話かも?

  • 「空白を表示」をオンにしたときに、スペースとタブの代わりに ‣ と • を表示している辺りのロジックが悪さをしている?
  • 一部はCP437の文字が表示されてる

どうでもいいものの一応はバグ報告:

  • Visual Studio Developer Community に: 10156578

まあ報告しておいてなんですけども、優先度付かないでしょうね、こんなの。

stackalloc の自然な型

$
0
0

今日は stackalloc T[N](stackalloc T[N]) に差があるとか、 (stackalloc T[N]).M() が許されるとか、 そんな感じの話。

ターゲット型推論と自然な型

C# の文法の中には、「基本的にはターゲットを見て型決定するけども、別にターゲットがなくても型決定できる」ような文法がいくつかあります。 例えば整数リテラルがそうなんですが、以下のように、ターゲット(左辺)の型が決まっていても決まっていなくても大丈夫です。

// ターゲット(左辺)の型に合わせて「100」の型を決めてる。
byte x = 100;
short y = 100;
int z = 100;

// 一方で、var だとターゲットからは型決定できない。
// そういう場合の 100 は int になる。
var v = 100;

ちなみに、var v = 100; みたいに「普段ターゲットから型を決めている式が、決めれないときにデフォルトで何の型になるか」を指して「自然な型」(natural type)と言います。 上述の場合、「整数リテラルの自然な型は int 」ということになります。

他だと、補間文字列リテラルも「ターゲット型推論 + 自然な型持ち」です。

using System.Runtime.CompilerServices;

var x = 100;

// ターゲット(左辺)の型に合わせて「$"abc{x}"」の型を決めてる。
string s = $"abc{x}";
IFormattable f = $"abc{x}";
DefaultInterpolatedStringHandler h = $"abc{x}";

// 一方で、こちらはターゲットからは型決定できない。
// そういう場合の $"abc{x}" は string になる。
var v = $"abc{x}";

stackalloc

stackalloc は元々 unsafe 限定機能で、 当然利用者も少ない機能でした。

ところが C# 7.2 で Span<T> 構造体とか安全な stackallocとか、 安全性を犠牲にせずにパフォーマンスを向上させれる文法が追加されて、 利用範囲が急に広がりました。

そして、安全な stackalloc の方が後入りなのもあって、stackalloc の自然な型はポインターのままです。

unsafe
{
    // stackalloc の昔からの用法。
    // 元々がこういう文法なので、 stackalloc の結果は T* (ポインター)。
    int* i1 = stackalloc int[4];

    // 型推論でも T* 扱い。
    // ↓の i2 は int* になる。
    var i2 = stackalloc int[4];
}

// C# 7.2 から
// ターゲットが Span のときに限り、safe コンテキストで stackalloc が使える。
Span<int> s = stackalloc int[4];

// ところが、stackalloc の自然な型はポインターのまま。
// 以下の行は「safe コンテキストでポインターは使えません」エラー。
var p = stackalloc int[4];

その後、C# 8.0 で、式の途中に stackalloc を書けるようになりました。 (C# 8.0 未満では、ここまで上げてきた例のように、変数に直接代入する場所にしか書けませんでした。)

// C# 8.0 未満でも書けた書き方:
Span<int> s = stackalloc int[4];

static void M(Span<int> s) { }

// こういう書き方は C# 8.0 以降でだけ書ける。
M(stackalloc int[4]);

こういう歴史的な流れから、現状の stackalloc がどうなっているかというと…

式の途中の stackalloc

C# 8.0 のとき、「式の途中に stackalloc を書いた場合に限り、自然な型を Span<T> にする」という決定をしていたりします。

例えば、以下のようなコードを書くと、M(int*)M(Span<int>) の呼び分けが掛かります。

unsafe
{
    // こちらは昔ながらの型決定で、 stackalloc の自然な型はポインター。
    var p = stackalloc int[4]; // int* 扱い。
    C.M(p); // M(int*) の方が呼ばれる。

    // こちらは「式の途中」ということで、C# 8.0 以降のルールで、自然な型が Span<T> に。
    C.M(stackalloc int[4]); // M(Span<int>) の方が呼ばれる。(なので実は unsafe 不要。)
}

class C
{
    public static unsafe void M(int* _) { }
    public static void M(Span<int> _) { }
}

で、この「式の途中なら Span<T>」な仕様を使うと、以下のようなこともできたりします。

  • var + stackalloc の自然な型を Span<T> にする
  • stackalloc に対して拡張メソッドを呼ぶ

var + stackalloc を Span に

式の途中なら自然な型が Span<T> になるということは… 実は () の有無で自然な型を変えれます。 () を付ければ safe。

// 前述のとおり、自然な型が int* で、unsafe 必須。
// (今は unsafe を付けていないのでコンパイル エラー。)
var p = stackalloc int[4];

// こっちは自然な型が Span<int>。
// var に対して使っても Span<int> になるので safe。
var s = (stackalloc int[4]);

そしてまあ、型推論推進派(左辺と右辺で2度同じ型名を書きたくない)にとっては、 安全な stackalloc を使いつつも型推論を掛けるための回避策になります。

// こう書いてもいいけども…
Span<int> s1 = stackalloc int[4];

// こっちの方が短いという。
var s2 = (stackalloc int[4]);

// まして、型名が長いときは… だいぶ差が大きい。
Span<LongLongStructName1234567890qwertyuiopasdfghjklzxcvbnm> s3 = stackalloc LongLongStructName1234567890qwertyuiopasdfghjklzxcvbnm[4];
var s4 = (stackalloc LongLongStructName1234567890qwertyuiopasdfghjklzxcvbnm[4]);

struct LongLongStructName1234567890qwertyuiopasdfghjklzxcvbnm { }

stackalloc に対して拡張メソッドを呼ぶ

そして、拡張メソッドも呼べるみたいですよ。

var x = (stackalloc int[4]).M(123);

static class C
{
    public static ReadOnlySpan<T> M<T>(this Span<T> span, T value)
    {
        span.Fill(value);
        return span;
    }
}

できる気はしていたものの、ほんとにできた…

というか、以下のようなコードを書いててふと思いつき。

using System.Text;

// u8 リテラルの自然な型は ReadOnlySpan<byte> だったはず。
// なら拡張メソッド M も呼べるはず。
"abcあいう"u8.M();

// そういや stackalloc にも自然な型あるはずよな…?

static class C
{
    public static void M(this ReadOnlySpan<byte> span)
    {
        foreach (var x in span)
        {
            Console.Write($"{x:X2} ");
        }
        Console.WriteLine();
        Console.WriteLine(Encoding.UTF8.GetString(span));
    }
}

ちなみに、拡張メソッド解決の仕様的に、以下のようなコードだとダメ(コンパイル エラー)だったりします。 Span<T> から ReadOnlySpan<T> への暗黙の型変換は、拡張メソッド解決の際には使われません。

using System.Text;

// これは呼べない。
// Span<byte> → ReadOnlySpan<byte> には暗黙の型変換があるものの、
// 拡張メソッド解決の際に暗黙の型変換を挟むことは許容していない。
(stackalloc byte[4]).M();

static class C
{
    public static void M(this ReadOnlySpan<byte> span)
    {
        foreach (var x in span)
        {
            Console.Write($"{x:X2} ");
        }
        Console.WriteLine();
        Console.WriteLine(Encoding.UTF8.GetString(span));
    }
}

拡張メソッドは暗黙型変換を見ない

$
0
0

こないだ、C# で (stackalloc T[N]).M() とか書けるという話を書いたわけですが。 その過程で出てきた「そういえばこんなのも」話をもう1個。

文字列補間の拡張メソッド呼びがちょっと変という話になります。

拡張メソッドの解決

拡張メソッドの存在意義は、 「語順を変更して、x.M().N() みたいな呼び出しができる」という点です。 ほとんどの場合は本当に「語順」だけの問題で、通常のメソッド呼び出しの形でも同じコードが書けます。

// 拡張メソッド呼び。
1.M();

// 同じものを通常のメソッド呼び出しで書く。
Ex.M(1);

static class Ex
{
    public static void M(this int _) { }
}

ただ、まあ、通常のメソッド呼びと拡張メソッド呼びでは、ちょっとだけ「解決ルール」みたいなやつが違ったりします。

無変換の場合、つまり、 「1int 引数に渡す」とか「""string 引数に渡す」みたいなときには変な挙動はしないんですが、 問題は型変換が絡む場合です。

解決できる例

先に、大丈夫な例から行きます。

親クラスや、実装するインターフェイスへの変換は問題なく行けて、 拡張メソッド呼び出しもできます。

// 親クラスや、実装しているインターフェイスへの変換は、拡張メソッド呼び出しできる。
1.Object();
1.Interface();

Ex.Object(1);
Ex.Interface(1);

static class Ex
{
    public static void Object(this object _) { }
    public static void Interface(this IComparable _) { }
}

オーバーロードがあるときには 「階層が近い方優先」で、これも通常メソッド呼び・拡張メソッド呼びで共通です。

// どっちも IComparable の方が呼ばれる。
1.M();
Ex.M(1);

static class Ex
{
    public static void M(this object _) => Console.WriteLine("object");
    public static void M(this IComparable _) => Console.WriteLine("IComparable");
}

ユーザー定義の型変換

拡張メソッドの解決時、ユーザー定義の型変換はみません。 一方で、通常のメソッド解決の時には見るので、 「拡張メソッド呼びだけできない」みたいなことがあります。

標準ライブラリでいうと、DateTimeDateTimeOffset とか、 Span<T>ReadOnlySpan<T> とか、 stringReadOnlySpan<char> とかがあります。

// 通常のメソッドとしてなら呼べる。
Ex.M("");                 // string → ReadOnlySpan<char>
Ex.M(stackalloc char[1]); // Span<char> → ReadOnlySpan<char>
Ex.M(DateTime.Now);       // DateTime → DateTimeOffset

// 拡張メソッドでは呼べない…
"".M();
(stackalloc char[1]).M();
DateTime.Now.M();

static class Ex
{
    public static void M(this ReadOnlySpan<char> _) { }
    public static void M(this DateTimeOffset _) {}
}

これがまあ、こないだのブログとのつながりでして。 「(stackalloc char[1]).M() が呼べない?そうだっけ?」からの、 「ReadOnly を削ったら呼べた」ということがありました。

ターゲットからの型推論

ターゲットからの型推論系の処理も、拡張メソッドでは働きません。

new()default 辺りはダメです。

// 通常のメソッドとしてなら呼べる。
Ex.M(new());      // new object()
Ex.M(default); // null

// 拡張メソッド前に型推論は働かない。
// エラーに。
new().M();
default.M();

static class Ex
{
    public static void M(this object? _) {}
}

ターゲットからの型推論 + 自然な型

ターゲットからの型推論は効かないものの、 自然な型を持っているやつはどうなるかというと…

基本的に、自然な型の時だけは拡張メソッド呼びもできます。

// 通常のメソッドとして、当然呼べる。
Ex.M(1);
Ex.M($"{1}");

// 整数リテラルの自然な型は int で、 int の拡張メソッドなら呼べる。
1.M();

// 同、string の拡張メソッドなら呼べる。
$"{1}".M();

static class Ex
{
    public static void M(this int _) { }
    public static void M(this string _) {}
}

例えば整数リテラルは shortbyte 型に変換できますし、 文字列補間 $""IFormattable や文字列補間ハンドラーに変換できます。 ところが、こういう場合は拡張メソッド呼びできません。

using System.Runtime.CompilerServices;

// 通常のメソッドとしてなら呼べる。
Ex.M(1);
Ex.M($"{1}");

// ターゲットからの型判定がかかるような例では拡張メソッドは呼べない。
1.M();
$"{1}".M();

static class Ex
{
    public static void M(this byte _) { }
    public static void M(this DefaultInterpolatedStringHandler _) {}
}

ラムダ式の自然な型

ちなみに、自然な型決定できるようになったにもかかわらず、 ラムダ式は自然な型に対しても拡張メソッド呼びはできません。 これは意図的で、() => {}.M() みたいな文法を認めたくなかったみたいです。 (() => {}).M() でもダメ。

// これは行ける。
// 何なら Delegate とか object 引数相手でもこう書ける。
Ex.M(() => { });

// これはダメ。
// 自然な型は Action なはずだけど。
(() => { }).M();

static class Ex
{
    public static void M(this Action _) {}
}

特殊なオーバーロード解決順序

文字列補間のオーバーロード解決順序はちょっと特殊です。

C# 10 でパフォーマンス改善のためにハンドラー パターンを導入したわけですが、 その時に検討された内容:

  • たいていのクラスがすでに string のオーバーロードを持っている
  • 普通に考えれば $"" の自然な型は string で、オーバーロード解決でも string 引数が優先されるべき
  • ところが string オーバーロードが呼ばれたらパフォーマンス改善されない
  • 何なら C# 6 で文字列補間を導入したときにも、IFormattable オーバーロードが呼ばれなくて困った

このような背景がありまして。 結果的に、「文字列補間ハンドラーがあれば、それを優先的に使う」という特殊処理が挟まっています。

using System.Runtime.CompilerServices;

// ハンドラー優先の特殊処理が働く。
Ex.M($"{1}"); // interpolation の方が呼ばれる

static class Ex
{
    public static void M(string _) => Console.WriteLine("string");
    public static void M(DefaultInterpolatedStringHandler _) => Console.WriteLine("interpolation");
}

これは本当に特殊処理です。 例えば、整数リテラルの場合は普通に int が優先されます。

// 普通に考えれば「自然な型」優先。
// 実際、整数リテラルは int 優先。
Ex.M(1); // int が呼ばれる

// int におさまらない桁のリテラルを書くと long リテラルになって、long オーバーロードが呼ばれるのに。
Ex.M(0x1_0000_0000); // long が呼ばれる。

static class Ex
{
    public static void M(int _) => Console.WriteLine("int");
    public static void M(byte _) => Console.WriteLine("byte");
    public static void M(long _) => Console.WriteLine("long");
}

で、この $"" に対する特殊処理が、拡張メソッド解決の際には働かないということは… 以下のように、通常メソッド呼びと拡張メソッド呼びで呼ばれるオーバーロードが変わるという症状を起こします。

using System.Runtime.CompilerServices;

// ハンドラー優先の特殊処理が働く。
Ex.M($"{1}"); // interpolation の方が呼ばれる

// そしてその特殊処理は、拡張メソッド解決時には働かない!
$"{1}".M(); // string の方が呼ばれる!

static class Ex
{
    public static void M(this string _) => Console.WriteLine("string");
    public static void M(this DefaultInterpolatedStringHandler _) => Console.WriteLine("interpolation");
}

特殊処理が挟まった背景を知らないと意味が分からない仕様ですよね。 一応、バグじゃなくて仕様通りです。

Viewing all 483 articles
Browse latest View live