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

未使用ジェネリック型引数で TypeLoadException が起こる問題

$
0
0

今日は C# の構造体がらみで、 できそうでできない、 できてもいいはずだけど直されない、 コンパイルまでは通るのに実行時にエラーになってしまう制限の話。

入れ子の構造体

C# で、構造体の中にその構造体自身のフィールドを持つことはできません。 レイアウトの決定が無限再帰を起こすので、これはダメで当然。

struct S { S _nested; }

これはそもそもコンパイル エラーになります。 当然。

使ってないジェネリック型引数でも TypeLoadExcpetion

問題は以下のような場合。 現在の C# (というか .NET の型システム)では、以下のような型はコンパイルはできるものの、実行してみようとすると実行時例外を起こします。 (構造体 S のメンバーに初めて触れた瞬間に TypeLoadException が飛ぶ。)

struct S { Empty<S> _empty; }
struct Empty<T> { }

Empty<T> の側が T のフィールドを持っていないので (というか空っぽなので、T が何かによらずサイズ1で固定)、 レイアウト決定で無限再帰は起こさないはずです。 実際、これは、

  • 原理的にはできてもいい
  • C# は禁止していない
  • CLI (.NET のランタイム仕様)でも禁止は名言されていない
  • 現在の .NET のランタイムの実装が過剰防衛している

という状態。 C# コンパイラー チームの人がそれを指摘する issue も立っていたりします。

実用例

まあ、Empty みたいな無意味なコードは誰も書かないとしても、 例えば、以下のようなシナリオでなら似たようなことをしたくなる人はいるはずです。

まず、以下のように、構造体の配列で木構造を表現する例を考えます。

// 配列に Parent と Next を持たせた型を入れて木構造を表現。
// A も B もツリー。
// A からは B も参照。
class Tree
{
    A[] A;
    B[] B;
}

struct A
{
    int Parent;
    int Next;
    int BIndex;
}

struct B
{
    int Parent;
    int Next;
}

実際にはさらに、「インデックスとは関係ない別の int も持ちたくなったりするはずで、なおのこと「この int は何?」みたいになると思います。

struct A
{
    // 木とは別に持ちたいデータ。
    int Value;
    int Length;
    ...

    // 木構造表現用。
    int Parent;
    int Next;

    // 別の木を参照
    int BIndex;
}

ということで、ParentNext が「配列 A[] のインデックス」であることが一目でわかるようにしたくなったりします。 よくやるのが、以下のように「int をラップした構造体を用意」みたいな手段。

struct Index<T>
{
    public int Value { get; }
    public Index(int value) => Value = value;
    public static implicit operator Index<T>(int value) => new(value);
}

この型を使って先ほどの Tree, A, B を書き換えると以下のような感じになります。

// 配列に Parent と Next を持たせた型を入れて木構造を表現。
// A も B もツリー。
// A からは B も参照。
class Tree
{
    A[] A;
    B[] B;
}

struct A
{
    int Value;
    int Length;
    Index<A> Parent;
    Index<A> Next;
    Index<B> BIndex;
}

struct B
{
    Index<B> Parent;
    Index<B> Next;
}

便利!

と思ったところで、冒頭の Empty<T> の例と同じ理屈の過剰防衛で、 TypeLoadException を起こします…

回避策

ちょっと不格好でもよければ解決方法は簡単で、 1段ダミーのクラスを挟むだけだったり。

struct Index<T>
{
    public int Value { get; }
    public Index(int value) => Value = value;
    public static implicit operator Index<T>(int value) => new(value);
}

// Index<Dummy<T>> とか Index<Empty<T>> よりは Index<Of<T>> の方がマシかなと…
class Of<T> { }
class Tree
{
    A[] AB[] B;
}

struct A
{
    int Value;
    int Length;
    Index<Of<A>> Parent;
    Index<Of<A>> Next;
    Index<Of<B>> BIndex;
}

struct B
{
    Index<Of<B>> Parent;
    Index<Of<B>> Next;
}

だいぶ不格好で嫌なので、 件の issue の優先度を上げてもらえるように👍を付けまくってもらえたりすると大変うれしかったりは…

この issue は2016年からずっと「Future」(いつかね、いつか)なんですよね。 これ、C# 6.0 (つまり、Roslyn 化/C# への移植)の頃に始めて報告されたというだけで、 実際には .NET Framework が生まれてこの方ずっとかも。


Unsafe クラスの敗北 (関数ポインター)

$
0
0

Gist に書き捨ててたコードの供養ブログ シリーズ、 今日のは特に人を選ぶやつ。

今日は C# 9 で入った 関数ポインター がらみの話です。

Unsafe クラス

C# の unsafe 機能、例えばポインターとかは、なかなか制限がきついです。 そのため、「実は .NET の型システム的にはできる」というものでも、 C# で書くことはできないことが結構あります。

それに対して、 .NET Core 以降、 Unsafe (System.Runtime.CompilerServices 名前空間)とかいう名前からして unsafe なクラスがあって、 内部的に IL を使ったり、 runtime intrinsics (JIT コンパイラーの特別扱い)で実装したりして、 元々 C# では書けなかったようなコードを、普通の C# で書けるようにしました。

この Unsafe クラスは、 unsafe コンテキストなしで、 普通の unsafe コードよりもよっぽど unsafe なことができちゃうという意味で良くも悪くも凶悪です。

ということで、皆様ご存じの通りUnsafe クラスを使えば C# でも C++ 的な遊びがいろいろと楽しめます。

using System.Runtime.CompilerServices;

var a = new A(123);

// readonly struct なので、↓はエラー。
//a.Value = 999;

// Unsafe.As を使えば、
// C++ でいう reinterpret_cast 的に何でもかんでも変換可能。
// (メモリレイアウトが想定通りかは利用者の自己責任。)
ref var x = ref Unsafe.As<A, int>(ref a);
x = 999;

// a.Value が 999 に書き変わってる。
// A { Value = 999 }
Console.WriteLine(a);

readonly record struct A(int Value);

どの方面に向かって「皆」と言っているのかは不明。

Unsafe クラスを使ったこの手の処理、 誤用すると盛大にクラッシュさせれるくらい安全性皆無になるので利用には注意が必要ですが、 パフォーマンス改善につながることが多くて、 一部界隈では結構多用されます。

ref struct の制限

ところが、Unsafe クラスでもできないことがありまして。 というか、Unsafe クラスはおろか、現状ではポインターを使っても解決できないものがありまして。

というのも、ref structはジェネリック型引数にもできないし、ポインターにもできません。

なので、先ほどと同じノリで ref struct に対して Unsafe.As (とか、それ相当の unsafe コード)を書こうとしてもうまくいきません。

using System.Runtime.CompilerServices;

var span = (stackalloc int[] { 0xDE, 0xAD, 0xBE, 0xEF });
var a = new A(span);

var spanFromA = Unsafe.As<A, Span<int>>(ref a);

ref struct A
{
    // private なので通常、この _span を取り出す方法ない。
    // なんならリフレクションを使っても無理。
    private Span<int> _span;
    public A(Span<int> span) => _span = span; 
}

関数ポインター

そんな時には関数ポインターを使えばいいらしいですよ。

C# 9 で入った関数ポインター、 delegate*<T1, T2, ...> みたいな、ジェネリクスに似た記法を使う割に、 この T1 とか T2 のところには ref も書けるし ref struct も書けるしで、相当自由みたいです。

要するに、Span<T> (ref struct)に対して、 Span<T>* (直接その型のポインター)は書けないし、 Unsafe.As<A, Span<T>> (型引数)も書けませんが、 delegate*<ref A, ref Span<T>> (関数ポインターの引数)なら書けます。

これを使えば、以下のように、ref struct に対しても Unsafe.As 的なことができるようになったりします。

var span = (stackalloc int[] { 0xDE, 0xAD, 0xBE, 0xEF });
var a = new A(span);

unsafe
{
    // function pointer の引数なら ref RefStruct も行ける。
    var f = (delegate*<ref A, ref Span<int>>)(delegate*<nint, nint>)&id;

    // 晴れて A の中から _span を抜き出し。
    var spanFromA = f(ref a);

    // span と同じ内容。
    foreach (var x in spanFromA) Console.Write($"{x:X2}");
    Console.WriteLine();

    // span が書き変わる。
    spanFromA[0] = 1;
    spanFromA[1] = 2;
    spanFromA[2] = 3;
    spanFromA[3] = 4;
}

// 上書きされた 01020304。
foreach (var x in span) Console.Write($"{x:X2}");
Console.WriteLine();

// nint 素通しメソッド。
// nint = unsafe コンテキスト内なら任意のポインター、任意の ref T を通せる。
static nint id(nint x) => x;

ref struct A
{
    // private なので通常、この _span を取り出す方法ない。
    // なんならリフレクションを使っても無理。
    private Span<int> _span;
    public A(Span<int> span) => _span = span;
}

native interop でしか使い道がないと思っていた関数ポインター、 こんなところで「これでしかできないこと」があるとは…

必ず、かの邪知暴虐の T4 を除かねばならぬと決意した

$
0
0

個人的に、前々から「T4 は将来性が見えなさ過ぎてもう使うのやめたい」と言い続けていたわけですが、 最近ようやく自分が保守している T4 を全部別の手段で書き換えたので、 今日はそれの話。

T4 (Text Template Transformation Toolkit)

テキスト テンプレートというと、ひな形的なテキストを簡易な文法で生成するようなものです。

例えば、

public static bool TryParse(this string s, out {{T}} x) => {{T}}.TryParse(s, out x);

みたいな文字列の、{{T}} のところに bool, byte, int, double を与えて、

public static bool TryParse(this string s, out bool x) => bool.TryParse(s, out x);
public static bool TryParse(this string s, out byte x) => byte.TryParse(s, out x);
public static bool TryParse(this string s, out int x) => int.TryParse(s, out x);
public static bool TryParse(this string s, out double x) => double.TryParse(s, out x);

とかを生成したいことがたまにあります。

今書いたみたいに4種・4行程度なら手書きでも全然かまわないんですが、 sbyte, short, ushort, ... と増やしていくとテキスト テンプレートに頼りたくなります。

C# でテキスト テンプレートというと、 T4 (Text Template Transfomration Toolkit)が有名ではあります。

T4 を使うと、上記の Parse は以下のように書けます。

<#
var types = new[] { "bool", "byte", "int", "double" };

foreach (var t in types)
{
#>
    public static bool TryParse(string s, out <#= t #> x) => <#= t #>.TryParse(s, out x);
<#
}
#>

T4 の今

元々 Entity Framework が内部で使っていたツールを公にしてしまったものですよね、確か。 今となっては本当に「してしまった」みたいな言い方にした方がいいと僕は本気で思っているんですけども。 どうも、.NET の中の人も、Entity Framework チーム以外あんまり乗り気で使っている風には見えず。 真面目に使う気があるのなら今時もうちょっと改良されててもよさそうなものなのに、 ちょっと塩漬け感があります。

例えば以下のような問題あり。

  • Visual Studio でしか動かず、しかも、手作業で .tt ファイルを開いて保存したタイミングでしかテキスト生成が走らない
    • 今時あれば、Roslyn Source Generator 化すれば dotnet build でテキスト生成できるのにやってない
    • Git とかで管理するなら、生成結果のテキストもコミットする運用になる
  • Visual Studio 自体の表示言語に生成結果が依存する
    • 編集して保存した人の表示言語によって生成結果が変わる
    • 無駄に差分が出て、Git とかの差分が悲惨
  • csproj 内にいろいろとゴミが残る

さらに、中間的に作られる「テキスト生成するための generator クラス」がまたかなり悲惨だったりします。

一例: 元 tt ファイル生成される generator クラス

例えば、元 tt ファイルで <#= t #> みたいになっているところは generator 内では ToStringWithCulture(t) みたいに展開されるんですが、 この ToStringWithCulture の中身は以下のようになっています。

public string ToStringWithCulture(object objectToConvert)
{
    if ((objectToConvert == null))
    {
        throw new global::System.ArgumentNullException("objectToConvert");
    }
    System.Type t = objectToConvert.GetType();
    System.Reflection.MethodInfo method = t.GetMethod("ToString", new System.Type[] {
                typeof(System.IFormatProvider)});
    if ((method == null))
    {
        return objectToConvert.ToString();
    }
    else
    {
        return ((string)(method.Invoke(objectToConvert, new object[] {
                    this.formatProviderField })));
    }
}

任意の型に対してカルチャー(IFromatProvider)指定するためだけにリフレクション。 しかも、MethodInfo のキャッシュもせず、毎回律義に GetMethod。 さらに、常に object 引数で受け取っているので、int (おそらく最多で渡される)とかだと都度ボックス化

T4 が作られた当初ならしょうがなかったのかもしれないですけどねぇ。 今なら、単に string.CreateとかStringBuilder.Appendでカルチャー指定もできるのに。 というか、むしろ、カルチャー依存やめろ、誰得

ちなみに、T4 生成の generator クラスと、自前で文字列補間を使って書いた generator でベンチマークを比べると、一例として以下のテーブルくらいの差が出ます。

Method Mean Error StdDev
T4 19,247.7 ns 74.46 ns 69.65 ns
Interpolation 330.6 ns 5.94 ns 5.55 ns

2桁差。 2倍ではなく、2桁。 マイクロ秒とナノ秒の補助単位違いレベル。

という感じで、T4、 さすがに中身がグダグダすぎ、かつ、近代化される気配がまるっきり皆無でつらいかなと思います。 テンプレートの文法とかはそこまでおかしくもないんですけどね。 さすがにもう使っていられないかなと…

脱 T4

ということで脱 T4 の話。

ただ、T4 の用途は2種類ありまして、それぞれ代替手段が異なります。

  • TextTemplatingFilePreprocessor: 上記でいう「generator を作る」ところまでやるモード
  • TextTemplatingFileGenerator: さらにその generator を実行して、最終結果を直接生成するモード

TextTemplatingFilePreprocessor

「generator を作るところまで」の方。

TextTemplatingFilePreprocessor な T4 はもう本当に存在意義がないですね。 先ほどすでに「文字列補間で自前で」とかやっていますが、 文字列補間で十分です。

特に、C# 10 で文字列補間のパフォーマンスが劇的に向上していますし、 C# 11 で入った生文字列リテラルによってテンプレートも書きやすくなっています。 本校冒頭で書いたテンプレートなら、普通に以下のように書けます。

using System.Text;

var s = new StringBuilder();

foreach (var t in new[] { "bool", "byte", "int", "double" })
{
    s.Append($$"""
            public static bool TryParse(string s, out {{t}} x) => {{t}}.TryParse(s, out x);

        """);
}

(カルチャー指定が必要なら Append メソッドの第1引数を追加。)

テキスト テンプレートとしては結構冗長ですけども、まあ、許容範囲で、 「素の C# だけで書けてる」ということを考慮すると十分満足の行くコードじゃないかと思います。

T4 からの移行も割かし簡単で、 こんな感じのコードで置換を掛けるだけで行けます。

  • <#=#> を、{{}} に置換
  • #>s.Append($$""" に置換
  • <#"""); に置換

(これだと不足もあるんですが、あとは手での書き換えでもなんとかなるレベル。)

TextTemplatingFileGenerator

「generator を実行までして最終結果を直接得る」の方。

こっちはさすがに素の C# ではできないんですが。 ビルド時に何かしらのテキストを生成するものというと、最近だと Roslyn Source Generator です。

T4 も、テンプレートの文法自体に不満はそこまでないので、Source Generator 実装に置き換わっていたらそれを普通に使うんですけどね… ここ数年定期的に「誰か T4 を Source Generator 実装しなおしてないかな」とか検索したりしてたんですが… いないですね。一向に。

そして、「ないので自分で作ろう」ってなったときに、なかなかきれいな T4 エンジン ライブラリが見つからず。 「だったらもっと楽に使えそうな別のテンプレート エンジンを使いたい」となりまして。 結局、Source Generator で一番使いやすそうだったのが scriban でした。

ということで作ったのがこれです:

例えば以下のような拡張子 .scriban のファイルを置くか、

static class Extensions
{
{{
for $t in ["bool","byte","int","double"]
~}}
    public static bool TryParse(this string s, out {{$t}} x) => {{$t}}.TryParse(s, out x);
{{ end }}
}

以下のようにクラスに属性を付けてコード生成できます。

namespace FileGenerator;

[ScribanSourceGeneretor.ClassMember("""
    {{
    for $t in ["bool","byte","int","double"]
    ~}}
        public static bool TryParse(this string s, out {{$t}} x) => {{$t}}.TryParse(s, out x);
    {{ end }}
    """)]
internal static partial class Extensions
{
}

JSON とかの中身確認ツール

$
0
0

今日は、「主に自分が使う用ツールを Blazor WebAssembly で作って Static Web Apps に置いたよ」系の話を一応ブログ化。

よくある「JSON とかのデータの中身を確認するツール」です。

しばらく、JSON と MessagePack の読み書きをするコードを書いてて、 デバッグがしんどくなって作ったのがこのツール。

いろんな形式を同時に扱うことがニッチ需要なのであんまり自分の需要にあったツールがなかったんですよね。なら、まあ、自作。

こないだの C# 配信 で、UTF-8 とか MessagePack バイナリとかを手打ちで入力してたら @xin9le, @okazuki 両氏にドン引きされたやつ。

バイナリ読み込み (Parser)

UTF-8 を ReadOnlySpan<byte> のまま扱ってて、 ブレイクポイントを仕掛けてデバッガーで中身を覗くとかやってると、 「97, 98, 99」みたいな数値列しか見れないことが多く。

デバッガーからコピペするとその数値が "97, 98, 99" みたいな文字列としてコピーされてしまったりするので、それをバイナリに戻す処理を入れています。

せっかくなので、

  • 10進数コンマ区切り 97,98,99
  • 10進数スペース区切り 97 98 99
  • 16進数コンマ区切り 61,62,63
  • 16進数スペース区切り 61 62 63
  • 16進数区切りなし 616263
  • UTF-8 文字列 abc
  • Base64エンコードされた文字列 YWJj
  • C# 風 0b0110_0001, 0x62, 99
  • 上記の中からある程度自動判定

とかを読めるようにしています。

文字列化 (Formatter)

読み込んだバイナリを表示する形式も選択式に。 UTF-8 だったらそれをデコードした文字列を見たいことがほとんどですし、 バイナリなら16進数を並べたもの辺りが使い勝手がいいと思います。

その他、「デバッグで使いたいけども、いちいちファイルとかから読むの面倒で、C# コード中に生で埋め込みたい」みたいな時のために、new byte[] { 0x61, 0x62, 0x63 } みたいな書式にする機能も持たせています。

デシリアライズ結果の可視化 (DOM)

用途が「JSON とかの中身確認」だったのでその機能を持っています。

json {"abc":0,"ABC":[null,false,true]}

みたいなデータを、

  • abc: 0
  • ABC:
    • 0: null
    • 1: false
    • 2: true

みたいな <li> タグにして表示します。

とりあえず JSON と MessagePack に対応。 Protocol Buffers と YAML くらいには対応してもいいかも。

自動判定というか、「エラーなくデシリアライズできた最初の1個を選択」みたいな処理も入れています。 (いかにも「自分用ツール」らしく、エラー処理が甘いんで結構フリーズするんですが…)

再シリアライズ (Writer)

再度シリアライズして、改めて Formatter にかける処理も入っています。

「MessagePack なバイナリを、JSON な UTF-8 で表示」みたいなことやると結構デバッグがはかどりまして。

【C# 12 候補】ラムダ式のデフォルト引数と params 引数

$
0
0

そろそろ、C# vNext 候補で上がってるものをちらほら紹介していこうかと。

今日は割かし確度高そうなものとして、ラムダ式がらみの話。 ラムダ式でもデフォルト引数と params への対応を考えているそうです。

提案ドキュメント:

C# 10 のときの話

C# 10 のときにラムダ式の改善がいくつか入りました。 以下のように、Web アプリがシンプルに書けるようになります。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// MapGet の引数は System.Delegate 型。
// Delegate に対してラムダ式が使える。
// 自然な型決定が働いて、この場合は Func<string> になる。
app.MapGet("/", () => "Hello World!");

app.Run();

この機能の延長で、

  • ラムダ式の引数にデフォルト値を与えられるように
  • ラムダ式の引数を params にできるように

の2つが追加で提案されています。

これまでのラムダ式の引数

C# 9 までの状態だと、 ラムダ式にデフォルト引数/params 引数が書けても役に立ちませんでした。

メソッドを使った例で説明すると、 以下のように、デフォルト引数/params 引数はデリゲート化する際に一切紛失します。

m();

// m() と呼べるのに、 Action には代入できない。
Action a1 = m;

// Action<int, int[]> には代入できるけど、
Action<int, int[]> a2 = m;

// Action<int, int[]> 越しには () では呼べない。
a2();

static void m(int x = 1, params int[] y) { }

デリゲートに代入して使うことが前提のラムダ式では、 そもそもデフォルト引数/params 引数を書けても全く役に立たないということになります。

そんな中、C# 10 ではラムダ式への属性指定ができるようになったわけですが、静的な型情報からは消えるという意味ではこの属性も同様だったりします。 ただ、属性は、静的な情報としては紛失したとしても、 リフレクションを使って属性を取る前提であれば意味があります。

using System.Reflection;
using Microsoft.AspNetCore.Mvc;

// f の型 (Func<string, string>) に FromBody 属性が反映されるわけではな。
Func<string, string> f = ([FromBody] string name) => "Hello World!";

// リフレクションで MethodInfo から引数や戻り値を取れば、それについてる属性を調べられる。
var p = f.Method.GetParameters()[0];

foreach (var a in p.GetCustomAttributes())
{
    // FromBodyAttribute
    Console.WriteLine(a.GetType().Name);
}

「リフレクションで」というのであれば、 デフォルト引数と params 引数も同様のはずです。

using System.Reflection;
using System.Runtime.InteropServices;

Delegate f = C.M;

foreach (var p in f.Method.GetParameters())
{
    Console.WriteLine(p.Name);
    Console.WriteLine(p.GetCustomAttribute<OptionalAttribute>());   // x のときに取れる
    Console.WriteLine(p.GetCustomAttribute<ParamArrayAttribute>()); // y のときに取れる
}

class C
{
    public static void M(int x = 1, params int[] y) { }
}

ラムダ式のデフォルト引数/params 引数を認める

ということで、C# 10 の時に属性を認めたのと同じく、 ラムダ式のデフォルト引数/params 引数を認めたいという話になりました。

そこから、もう1歩進めた提案もあって、 自然な型決定で、デフォルト引数/params 引数付きのデリゲートを作るという話もあります。

static void m(int x = 1, params int[] y) { }

// 今までだったら Action<int, int[]> になってた。
// これを、 delegate void Anonymous(int x = 1, params int[] y) で生成したい。
var f = m;

// 今まででも、↓なら呼べる。
f(1, new[] { 2 });

m(1, 2);
m(x: 1);
m(y: 2);

// ↓はこれまではダメで、C# 12 でできるようにしたい。
f(1, 2);
f(x: 1);
f(y: 2);

Console.WriteLine(f.GetType());

割かし実装も進んでいるはずなので、これは近いうちにプレビューが来ると思われます。

【C# 12 候補】IEnumerable 向けリスト パターン

$
0
0

C# vNext (12 候補)紹介シリーズ。

今日はリスト パターンがらみ。

提案ドキュメント:

C# 11 のときの話

C# 11 でリスト パターンが入りました。

is [] みたいに、[] を使って配列とか List<T> に対するパターン マッチを行います。 ただ、C# 11 時点では、

  • countable: Length もしくは Count で長さを取れる
  • indexable: [int index] (整数引数のインデクサー)で i 番目の要素を取れる
  • sliceable: [Range range]Slice(int start, int length) でスライスを作れる

みたいな割と厳し目な条件を満たす型に対してだけリスト パターンを使えました。 以下の例ではリスト パターンとその展開結果をコメントに書いていますが、 見ての通り、 Length[] を使ったコードと等価です。

using static System.Console;

static void m(int[] x)
{
    WriteLine(x is []); // x.Length == 0
    WriteLine(x is [1]); // x.Length == 1 && x[0] == 1
    WriteLine(x is [1, ..]); // x.Length >= 1 && x[0] == 1
    WriteLine(x is [_, .. var y]); // y = x[1..]
    WriteLine(x is [_, .. var z, _, _]); // y = x[1..^2]
}

対 IEnumerable

提案当初(コミュニティ提案だったりします)では、 リスト パターンは IEnumerable に対しても使える提案がありました。 別にリジェクトされたわけでもないんですが、countable, indexable, sliceable に対するものと比べると課題が多いので「後回し」にされています。

まあ、元々提案にあったものなので、引き続き検討しようかという感じで C# vNext 候補です。 元々の提案では、何らかのヘルパー クラスを間に挟んで、 x is [0, 1, ..] みたいなコードを以下のような感じで展開することを考えています。

var helper = new ListPatternHelper(x, 2, 0);

helper.TryGetStartElement(index: 0, out var element0) && element0 is 0 &&
helper.TryGetStartElement(1, out var element1) && element1 is 1

課題

リスト パターンを IEnumerable でも使えるようにしようとすると、スライスが絡むときが難しそうです。 例えば、 x is [0, 1] だとそんなに問題はなくて、 「最初の2個分 MoveNextCurrent するだけ」になるんですが。 一方で x is [.., 1] だと、 LINQ でいうところの Last になるわけで、 LINQ でもそうなんですけども、無限シーケンスで困ります。

// 普通、無限シーケンスは Take(有限の値) とかで一部分だけ取り出して使う。
var x = m().Take(100);

// 無限なものの Last があるわけなく、永久ループになる。まずい。
var y = m().Last();

static IEnumerable<int> m()
{
    var i = 0;
    while (true) yield return i++; // whiel(true) なので永久に途切れない
}

この辺りを中心に、LDMで検討:

スライスの後ろにパターンがある場合

前述のように x is [.., 1, 2] とかになっているパターンをどうするか:

  • 展開結果がちょっと複雑
    • → 手書きするよりはコンパイラーに頑張ってもらう方がマシ
  • 無限シーケンスとかみたいに footgun (勢い余って自分の足を打ち抜いちゃいそうな道具)になりそう
    • → 元から。LINQ もそうだし、なんだったら .NET Framework 1.0 の頃からこの手の footgun はある
  • LINQ よりもパフォーマンスが落ちる可能性
    • LINQ to Object の場合、内部的に is IList 分岐とかで「indexable ならそれ前提のコードを使う」みたいな最適化をしてる
    • この問題は大きいと思っている。「パターンを使うとパフォーマンスが悪くなる」という状況は避けたい
    • IEnumerable 向けリスト パターンでもその手の最適化がかかるようにしないといけない

まあとりあえず、最初の実装としては x is [1, 2, ..] みたいな「スライスの前」だけを認めて、 x is [.., 1, 2] みたいな「スライスの後」は後々改めて検討するのでもいいかも見たいな雰囲気です。

「スライスの後」の方は、 結局は、「最初に紹介した ListPatternHelper みたいなヘルパー クラスの中で、LINQ と比べてパフォーマンス悪化させないような最適化がかかってほしい」ということになるんですが、「BCL チームと連携して作る」とのこと。

.. 部分をキャプチャ

リスト パターンでは、x is [1, 2, ..var y] みたいに書いて、y = x[2..] みたいなスライスをキャプチャすることもできます。 とうことで、「スライスをキャプチャ」は、「配列とかリストのスライス」と密接に紐づいています。

で、このスライスなんですが、推奨としては「Slice メソッドの戻り値は元の型と同じにするべき」ということになっています。 配列であれば x[i..j] の結果も配列、 List<T> であれば x[i..j] の結果も List<T>Span<T> であれば x[i..j] の結果も Span<T> ということです。

(その結果、配列や List<T> に対して [..] を使うとコピーが発生してパフォーマンスはそんなによくないですが、「型が同じ」の方が驚きは少ないだろう、パフォーマンスが必要なら Span を使えばいいだろうということになっています。)

ところが、IEnumerable の場合、スライスを具体的に何の型にすればいいのかが決まらないので困ると。

これも結局、「よいヘルパー クラスができてから改めて考える」みたいな空気感で終わっています。

【C# 12 候補】半自動プロパティ

$
0
0

今日は半自動プロパティの話。

約1年前にも書いてる通り、場合によっては C# 11 で入っていたかもしれないものです。

需要はそれなりに高いんですが、 案外課題があって結局スケジュール的に11からははずれ、「その後どうなったの?」とか思われていそうな機能です。 (12候補としては結構有力。)

半自動プロパティの話自体は去年度にしているので、 今日書くのはその「課題」をつらつらと。

半自動プロパティ概要

去年の繰り返しになるので概要のみ。 要は、手動で書く通常のプロパティ(以下、手動プロパティ)と自動プロパティの中間で、 バッキング フィールドのアクセスに 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; }
}

field の “キーワード性”

半自動プロパティに類する提案は他にもありつつも、 現状はとりあえず「field キーワード」案で話が進んでいます。 キーワード追加。

ところが、この世に出ている C# コードの中には「field という名前のフィールドや変数」がそれなりにあって(オープンソースになっているコードとかを検索すると相当量出てくるそうで)、さすがに「field を文脈抜きに無条件にキーワード扱い」とかやるのは、破壊的変更としては許容できるレベルを超えていて、現実的ではないです。 field という単語は、最近提案されている新機能の中では断トツで(recordrequired すら霞むくらい)影響力が大きいかもしれません。

一方で、文脈キーワードの仕様はなかなかに複雑になりがちで、 今、ちょっと単純化したいという話もあるくらいです。 そんな中、半自動プロパティでは早速苦戦しそうな雰囲気。

半自動プロパティの field は、極限まで突き詰めて「有効な時だけキーワード扱い」をやろうとすると var とか record とかよりもだいぶ難しいみたいです。 一例として挙がっているのは以下のようなコード。

unsafe struct S
{
    object Prop
    {
        get
        {
            S s = new();

            // このステートメントは「構造体 S が unmanaged のときだけ有効」
            // 言い換えると、「構造体 S が参照型のフィールドを持たないときだけ有効」
            // (C# 11 からは警告のみになったものの、元々はエラー。)
            var ptr = &s;

            // field が「S とは無関係な定数とか」だと &s が有効。
            // ところが、field がキーワードで、バッキング フィールドが自動的に作られると &s が無効になる。
            // 「&s が無効にならないようにこれは認めない」みたいなことまでやるのは解析が「循環」してしまう。
            return field;
        }
    }
}

なのであんまり正確にやるのはやめておいた方がいいとして、 簡素化した案でいうと以下のようなものがあります。

  • セマンティクスを見るのは「field という名前のクラスと、field という名前のフィールドがあるかどうか」だけ
  • あとは、構文的にだけ解析して、「スコープ内に field という名前の識別子がいるかどうか」で判定

簡素化するために「スコープを無視して解析」みたいな案もあるみたいなんですが、 結局は、以下のように「スコープも考慮に入れる」、「内側のスコープやローカル関数でのシャドーイングは認める」という予定だそうです。

object Prop
{
    get
    {
        {
            // この field は {} 内でだけ有効。
            int field = 1;
        }

        // このフィールドは m の内側でだけ有効。
        static void m(int field) { } 

        // {} とかローカル関数の外側には "field" がいないので、
        // ここの field はキーワード。
        return field;
    }
}

というのも、同スコープ内の解析に限っても、それなりに解析が大変そうな文法がいくつかあって、「労力は変わらない」とのこと。

class C
{
    int Prop
    {
        get
        {
            var x = (field: 1, 2); // タプル要素名
            var y = new { field = 1 }; // 匿名型のプロパティ
            var z = new Foo() { field = 1 }; // オブジェクト初期化子でのフィールド/プロパティ参照
            if (x is { field: 1 }) { } // プロパティ パターンでのフィールド/プロパティ参照

            // 上記の field はいずれも、field という名前の変数が新たに導入されたりはしない。
            // このスコープ内に "field" はいないので、ここの field はキーワードでいいはず。
            return field;
        }
    }
}

class Foo { public int field; }

初期化子の挙動

C# の構造体には「すべてのフィールドを初期化しきるまで関数メンバー(メソッドやプロパティ)を呼べない」という仕様がありました。 (ただし、C# 11 で緩和されました。)

struct S
{
    int _x;

    public void M() { }

    public S()
    {
        // C# 10 まではコンパイル エラーになってた。
        M(); // _x の初期化より前
        _x = 0;
    }
}

そんな中、C# 6 で get-only プロパティの導入とともに、 「コンストラクター内での自動プロパティへの代入は、それのバッキング フィールドへの直接代入への最適化を認める」という仕様も入っています。

struct Point
{
    public int X { get; private set; }

    public Point(int x)
    {
        // C# 5.0まではエラーに。
        X = x;

        // これを認めるために、X = x の部分は「Xのバッキングフィールド = x」に展開される。
    }
}

その流れで、プロパティ初期化子も「バッキング フィールドへの代入に展開」されます。 例えば以下のようなコードを書いたとします。

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

record struct R(int X)
{
    public int X { get; private set; } = X;
}

このコードは、以下のようなコードとほぼ同じ挙動になります。

struct S
{
    private int _x;
    public int X { get => _x; private set => _x = value; }
    public S()
    {
        _x = 1; // X = 1 ではなくて、_x = 1
    }
}

struct R
{
    private int _x;
    public int X { get => _x; private set => _x = value; }

    public R(int X)
    {
        _x = X; // this.X = X ではなくて、_x = 1
    }
}

という背景の中、半自動プロパティの場合はどうしようかという問題があります。 例えば以下のようなコードを認めたいんですが、 じゃあ、初期化時に OnXChanged は呼ばれるのかどうか。

struct S
{
    // 流れ的にはこういうプロパティ初期化子も認めたい。
    public int X
    {
        get => field;
        private set
        {
            field = value;
            OnXChanged();
        }
    } = 1;

    public S() { }

    public void OnXChanged()
    {
        Console.WriteLine("何か副作用起こす");
    }
}

C# 11 での変更前は「自動プロパティと同様にせざるを得ない」と言われていました。 つまるところ、プロパティ初期化子はバッキング フィールドへの直代入に展開されて、 結果的に、OnXChanged は呼ばれないということになります。

C# 11 でこの要件は必然ではなくなったわけですが、 それでも「自動プロパティと同様」の仕様(OnXChanged は呼ばれない)になりそうな雰囲気です。

override

override したときの挙動をどうしようかという問題もあります。 というのも、例えば以下のコードを考えます。

class Base
{
    // 自動プロパティなので、バッキング フィールドが作られる。
    public virtual int Prop { get; set; }
}

class Derived : Base
{
    // override してる時点で Base.Prop とは別物。
    // それをまた自動プロパティにすると、Base.Prop のものとは別に追加でバッキング フィールドができる。
    public override int Prop { get; set; }
}

自動プロパティの作るバッキング フィールドは BaseDerived で独立しています。 さらに、virtual なプロパティは「get だけ override」みたいなことができます。

var x = new Derived { Prop = 2 }; // set は base.Prop のものがそのまま呼ばれる。
Console.WriteLine(x.Prop);        // get は Derived.Prop が呼ばれて、4 になる。

class Base
{
    public virtual int Prop { get; set; }
}

class Derived : Base
{
    // get だけ override して、base のものの二乗を返す。
    public override int Prop { get => base.Prop * base.Prop; }
}

そんな中、半自動プロパティでの override はどうしよう?という話になります。

var x = new Derived { Prop = 2 };
Console.WriteLine(x.Prop);

class Base
{
    public virtual int Prop { get; set; }
}

class Derived : Base
{
    // get だけ override して(全)自動プロパティというのはできない。
    // じゃあ、get だけ "半"自動プロパティは?
    // これは Base.Prop とは別のバッキング フィールドになる?
    public override int Prop { get => field * field; }
}

これはさすがにどう転んでもわかりにくいので、 いっそのこと、「半自動プロパティでの override はすべてのアクセサー(get/set 両方)の override が必須」とするそうです。

nullability

半自動プロパティの導入の動機の1つに遅延初期化、 すなわち、以下のようなコードを書きたいというものがあります。

public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

この用途の場合、バッキング フィールドの型は string? であるべきなんですよね。

ところが、現状は「半自動プロパティから作られるバッキング フィールドの型はプロパティの型と同じ」という仕様なので、string になります。

参照型に関しては元から ? の有無はフロー解析の差だけなのでそこまで問題ではないんですが、 値型の場合は困ります。

public class LazyInit
{
    // field も int なので、 ?? が意味をなさない。
    public int Value => field ??= ComputeValue();
    private static int ComputeValue() { /*...*/ }
}

これは、「field キーワード」路線でやる以上は解決しようがなさそうで、 それとは別に「プロパティ スコープ フィールド」(半自動プロパティと同じ要件に対する別案)が必要かもしれません。 とはいえ、とりあえず「field キーワード」優先で、 プロパティ スコープ フィールドはやるとしてもその後ということになっています。

using alias を任意の型に対応

$
0
0

今日は using alias の話。

これはちらほら実装が始まっているので近々触れるものが出てくるんじゃないでしょうか。

既存の using ディレクティブ

using alias は、using ディレクティブを書くときに using T = System.DateOnly; みたいに書いて、以後は T だけで型名を参照できるやつ。 現状何が問題かというと…

まず、以下のコードであれば現状でもコンパイルできるんですが…

using List = System.Collections.Generic.List<int>;
using ListA = System.Collections.Generic.List<int[]>;
using ListN = System.Collections.Generic.List<int?>;
using ListT = System.Collections.Generic.List<(int, int)>;

そのくせ以下のコードはコンパイルできません。

using Primitive =  int;
using Array = int[];
using Nullable = int?;
using Tuple = (int, int);

要するに、ジェネリック型引数なら制限がほとんどないのに、トップレベルの時にだけ、以下のものを書けないという制限がありました。

  • int みたいにキーワードを使ったプリミティブ型 (⇔ System.Int32 なら書ける)
  • null 許容型 (T?) (⇔ System.Nullable<T> なら書ける)
  • タプル ((T1, T2)) (⇔ System.ValueTuple<T1, T2> なら書ける)
  • 配列 (T[])

まあさすがにいい加減これを認めようという話になっています。

一番需要があるのはタプルですかね。 あと、最近では関数ポインターなんかも delegate*<int, int, void> みたいな感じで名前が長くなりがちなので、これに対しても使いたいみたいです。

微修正

int とか int? とかに対応するだけなら大した変更は要らないみたいです。 構文的には1行書き変わるだけ

using_alias_directive
-    : 'using' identifier '=' namespace_or_type_name ';'
+    : 'using' identifier '=' (namespace_name | type) ';'
    ;

たぶん、「元々 using 専用に特殊処理していたけども、普通の型名参照と同じものに置き換える」みたいな感じでしょうか。

これは… もっと早くから対応してくれててもよかった疑惑が…

トップレベルの null 許容参照型

参照型に対しては、トップレベルでは ? をつけれないようにするみたいです。 まあ、今でも、typeof(string) は書けても typeof(string?) とは書けないので、 それと同じです。

using List = System.Collections.Generic.List<string?>; // これは OK。
using S = string?; // これはダメ。

ポインター

要望として関数ポインターのエイリアスを作りたいわけですが。 unsafe なものを単に using T = int*; とか書いていいのかどうかという議題がありました。

これに対しては結局、using unsafe という構文を導入するみたいです。

using unsafe T = int*;
using unsafe F = delegate*<int, int, void>;

今後の課題: 型引数

エイリアスをジェネリックにして型引数を持たせたいという話もあります。 以下のような、エイリアスの右辺にも <T> を付けたいというやつ。

using List<T> = System.Collections.Generic.List<T>;

これはこれで要望はあって、Backlog (すぐに手を付けるほどの優先度にはない)とはいえ、 Champion (C# チームの担当がついてる状態)にはなっています。

ただ、これの対応は「微修正」では済まないので、 C# 12 マイルストーンからは外れるみたいです。


【C# 12 候補】コレクション リテラル

$
0
0

今回はコレクション リテラルの話。

・提案 issue: [Proposal]: Collection literals #5354

今日の話も、提案自体は去年から結構前向きに検討されてたものです。 リスト パターンの実装の過程で出てきた案で、元から「C# 11 には間に合わないかも」みたいな空気感だったもの。 昨年11月に C# 11 が世に出た後、改めて進捗が出始めたので、今日はその辺りの話になります。

ちなみに、Language Feature Status で、最近 "C# Next" の欄に並びました。 実装もちらほら始まっているので、割かし C# 12 入りが有望だと思います。

コレクション リテラルおさらい

去年から大体決まってそうなところをおさらい。

文法的には [] を使う案が有力です。

using System.Collections.Immutable;

// いろんなコレクション型に対して共通して使える。
int[] array = [1, 2, 3];
Span<int> span = [1, 2, 3];
List<int> list = [1, 2, 3];
ImmutableArray<int> immutable = [1, 2, 3];

また、同時に、いわゆる "spread" と呼ばれる操作も導入されます。

// いろんなコレクション型に対して共通して使える。
int[] a = [1, 2];
int[] b = [3, 4];

// これだと、2重配列の [ [1, 2], [3, 4] ] になる。
int[][] nested = [a, b];

// これが "spread"。各コレクションを展開して、concat 的な操作をする。
// [ 1, 2, 3, 4] になる。
int[] spread = [..a, ..b];

// もちろん混在もあり得る。
// [ [1, 2], 3, 4] になる。
object[] spread = [a, ..b];

導入の動機は以下のようなものです。

  • 現在だと new T[] { ... }, new T { ... }, stackalloc T[] { ... }, T.Create(...) みたいにバラバラな書き方になるものを簡潔な書き方に統一する
    • 特に、stackalloc みたいな不自然な構文の必要性を減らす
  • リスト パターンと対称な構文を用意する
  • コレクション初期化周りのパフォーマンス改善
    • 現状書くとすると a.Concat(b).Append(c).ToArray() は書くのが煩雑な上にパフォーマンスが悪い
    • ReadOnlySpan 最適化みたいな知らないとまず書けない最適化を減らしたい
  • 型推論フレンドリーにしたい

その後

最近の C# チームは、有望そうな機能ごとに、その機能を専門に検討する「Working Group」という単位を作って作業をしています。 コレクション リテラルにも Working Group があって、2回ほど Working Group 内でのミーティング議事録が公開されています。

以下、これらの議事録から去年からの進捗を拾って列挙していこうかと思います。

  • 自然な型 (var x = [a]; のときの x の型)は List<T> がよさそう
    • var a = [x]; a.Add(y); ってやりたい
    • let (readonly local) が入るんなら、let x = [a]; の自然な型は ImmutableArray<T> がよさげ。Swift がそう
  • アロケーションを減らせるように、長さが既知なら new List<T>(length) みたいに capacity を渡せるようにしたい
    • IEnumerable<T> に対しても、Enumerable.TryGetNonEnumeratedCount を使って事前に長さをとれないか試みる
  • 当初予定では var x = new(length); x.Init(buffer); みたいなコード生成のつもりでいたけども、このメソッドは Init よりも Construct という名前の方がいいかも
  • 長さが未知の [..enumarable]ImmutableArray<T> みたいな Add が使えない型に対しても使えるようにしたい
  • lazy な enumerable に対して [..a, ..b] するとき、これも lazy であってほしいと望む人もいそうだけど、たぶん、(即時評価で)新しいコレクション作る
  • 新しい文法がが「一番パフォーマンスいい」状態にはしたい
    • (ImmutableArray<T> に対する [..enumerableOfT]ImmutableArray.CreateRange<T> よりも遅くなってはいけない。)
  • [..a]a のクローン手段として使えていい
  • []null と同様に、「どんな型のコレクションにでも代入できる特殊なリテラル」であるべき
  • var list = cond ? [str] : [obj]; の型はどう決める? target-typed? それか、common-base-type?
  • 一緒に Dictionary リテラルもやる。最有力候補は [key:value]という書き方
    • key:value の部分で KeyValuePair<TKey, TValue> なリテラルになりそう
    • List<T>, Dictionary<TKey, TValue>, KeyValuePair<TKey, TValue> の3つの型は first-class になりそう

Dictionary リテラル

特に、Dictionary リテラルの話は去年はまだなかったですかね、確か。 候補となる文法は以下のようなもの。 (このうち、[key: value] が有力。[ [key] = value ] もありかも。)

var dict1 = { "key1": "value1", "key2": "value2" };
var dict2 = ["key1": "value1", "key2": "value2" ];
var dict3 = [ ["key1"] = "value1", ["key2"] = "value2" ];
var dict4 = ["key1" => "value1", "key2" => "value2"];

Dictionary リテラルをやるのであれば、一緒に「Dictionary パターン」もやりたいそうです。

var dict = [ "key1": "value1", "key2": "value2" ];

if (dict is [ "key1": var value ])
{
}

[key: value] の場合には x.Add(new(key, value)) (もしくは前述の Construct)扱いで、 [ [key] = value ] の場合には x[key] = value 扱いという区別で両方認める可能性もあります。 この場合、「パターン」の方も、以下のような別パターンを考えます。

var dict = [ ["key1"] = "value1", ["key2"] = "value2" ];

if (dict is [ ["key1"]: var value ])
{
}

他に、以下のような話あり。

  • spread の併用で [..dict, prop: a] とか書くのも OK
  • Swift は [:] で空辞書リテラル作れる。C# もやる?
  • init void Init(KeyValuePair<TKey, TValue>[] values) で実装
  • コレクション インデクサー(List<string> list = [0: "first", 1: "second"]; みたいなの)も認める?
  • Dictionary 2個連結した [..dict1, ..dict2] は Dictionary であるべき

【C# 12 候補】params Span、改め、params ReadOnlySpan

$
0
0

今回は params の話。

params の改善話は紆余曲折ありまして。 去年の時点では params Span<T> で検討されていました。 ちょこっとだけマイナーチェンジされまして、現在は params ReadOnlySpan<T> です。

いろんな型で params 案(没)

現在の C# の params (可変長引数)は、params T[] (引数の型は配列)しか書けません。 これに対して、任意のコレクション型を使って、params List<T> とか params IEnumerable<T> とか書きたいという要望が長らくありました。

// (あくまでも過去の案)
M1(1, 2, 3);
M2(1, 2, 3);
M3(1, 2, 3);

static void M1(params IEnumerable<int> items) { }
static void M2(params List<int> items) { }
static void M3(params Span<int> items) { }

「この類の何かを書きたい」という要望は今でもあるんですが、 ただ、ここにきてコレクション リテラルという提案が出ています。 コレクション リテラルがあれば、別に params がなくても以下のように書くことができます。

// 呼び出し側をコレクション リテラルにしてしまう。
// 元の params 案との差は [] の2文字だけ。
M1([1, 2, 3]);
M2([1, 2, 3]);
M3([1, 2, 3]);

// params で任意のコレクションを扱うのはやめちゃう。
static void M1(IEnumerable<int> items) { }
static void M2(List<int> items) { }
static void M3(Span<int> items) { }

[] の2文字程度ならさぼらず書いてもいいんじゃないかという感じがします。 なので、「params の汎用化」という目的においてはもう別にやらなくてもいいんじゃないかという雰囲気になっています。

params ReadOnlySpan

「params の汎用化」が没り気味な一方で、 「既存の params T[] 利用個所のパフォーマンスを改善したい」という要件は残っています。 そこで出てくるのが params ReadOnlySpan<T> になります。

すなわち、

  • params に使えそうな中で一番パフォーマンス的に有利な ReadOnlySpan だけを残す
  • 既存の params T[] よりも、params ReadOnlySpan<T> の方がオーバーロード解決優先順位を上にする
    • 既存のメソッドに params ReadOnlySpan<T> なオーバーロードを足せば、利用側は再コンパイルするだけでパフォーマンス改善になる

という方針で進めるようです。

ちなみに、params ReadOnlySpan<T> で定義した引数は常に scoped みたいです。 ReadOnly で受け取っているので書き換えできず、scoped なのでメソッドの外には漏らせません。 その結果、呼び出し側で M(a, b, c) みたいな書き方から「a, b, c を含む ReadOnlySpan」を作るときの最適化がしやすくなっています (DLL のデータ領域を直接参照したり、複数回呼び出されるときに同じバッファーを使いまわしたり)。

固定長バッファー

以下のようなコードを書いたとき、

M(1, 2, 3);
M("a", "b", "c");

static void M<T>(params ReadOnlySpan<T> items) { }

概念的には、以下のように「スタック割り当て」をしたいです。

// int の場合はこれで問題ない。
ReadOnlySpan<int> temp1 = stackalloc[] { 1, 2, 3 };
M(temp1);

// 現状、参照型の stackalloc はできないので、何らかの対処が必要。
ReadOnlySpan<string> temp2 = stackalloc[] { "a", "b", "c" };
M(temp2);

static void M<T>(params ReadOnlySpan<T> items) { }

そこで、参照型にも使えるスタック割り当て手段を必要とするわけですが。 去年の時点で、 「Experiment with 'Unsafe.StackAlloc'」とか「[hackathon] ValueArray」みたいなプロトタイプもあったんですが、あんまり筋はよくなかったようで没っています。

そして現状、「特殊な属性を1個用意して、それをつけると .NET ランタイムが特殊対応して固定長バッファーを生成する」みたいな案で進んでいるようです。

ちなみに、params ReadOnlySpan<T>ReadOnlySpan<T> に対するコレクション リテラルは同じ戦略をとるそうで (やることは同じなので2重実装は避ける)、 「params の改善」と「コレクション リテラル」は2つ合わせて同時に進めるということになりました。

【C# 12 候補】 Extensions

$
0
0

今日は「拡張」(拡張メソッド的なものの改良)の話。 (今日のこれは、C# 12 で全て実装されるかどうか怪しく、 一部 13 以降になる可能性も結構高いです。)

結構昔から、

  • Extension everything: 拡張メソッドと同じような仕組みでプロパティ、インデクサー、演算子などを「拡張」したい
  • Roles: 「拡張」をある種の「型」扱いしたい

みたいな案があったんですが、結局、この Roles をベースに、Extensions とか Extension types という名称で実装が進みそうです。

原案で「Roles/Extensions」と呼ばれていたものは、「Explicit /Implicit extensions」となります。

extension キーワード

提案されている現状の文法では、新たに extension キーワードを使った「型定義」できるようにするみたいです。

例えば、int に対する「拡張」を書くのなら、以下のような書き方をします。

implicit extension Ex for int
{
}

なんでも拡張

現状の拡張メソッドの仕様では、名前通り、メソッドしか定義できません。 プロパティなどを「拡張」したいという要望は長らくあるんですが、 今の拡張メソッドの文法がプロパティなどに向いていなさ過ぎて、導入できずにいます。 また、静的メンバーにも対応していません。

static class Extensions
{
    // x.Method() と呼べる。
    // 第1引数を特別扱いしてる都合上…
    public static void Method(this int x) { }

    // 引数のないプロパティとか、
    public static int Property { }

    // インデクサーはどうするか悩ましい。
    public static int this[int index] { }

    // 元が static なものを拡張する手段もない。
    public static int operator +() { }
}

extension を使った定義では、インスタンス フィールドと自動プロパティ自動イベント(暗黙的にフィールドが必要)を除いて、どのメンバーでも使えます。

implicit extension Ex for int
{
    public void Method() { }
    public int Property => this;
    public int this[int index] => index;

    public static void StaticMethod() { }
    public static Ex operator+ (Ex x) => x;
}

ちなみに、インターフェイスも実装できる予定です。 既存の(第3者が作っていて自分では手を入れられない)型にインターフェイスを後挿しできます。

implicit extension Ex for bool : IFormattable
{
    public void ToString(string? format, IFormatProvider? formatProvider) => this ? "true" : "false";
}

これで、以下のような呼び出しができるようになる予定です。

int x = 0;

x.Method();
_ = x.Property;
_ = x[1];
int.StaticMethod();

IFormattable f = true;

拡張「型」

既存の拡張メソッドでも起こるんですが、 複数の拡張があるとき、同名のメソッドが被ってどちらを呼ぶべきか解決できない時があります。

int x = 0;

// 2つ同名のメソッドがあって優先度解決できないのでコンパイル エラー。
x.Method();

// 解決するためには途端に「普通の静的メソッド」呼びに戻る。
Ex1.Method(x);
Ex2.Method(x);

static class Ex1
{
    public static void Method(this int x) { }
}

static class Ex2
{
    public static void Method(this int x) { }
}

また、拡張メソッドは元々あるインスタンス メソッドよりも優先度が低いので、 同名のメソッドで「上書き」することもできません。

int x = 0;

// インスタンス メソッドの方が優先度が高く、この書き方で Ex1.ToString は呼べない。
x.ToString();

// 「普通の静的メソッド」呼びで一応解決は可能。
Ex1.ToString(x);

static class Ex1
{
    public static void ToString(this int x) => x.ToString("X2");
}

これらの例の通り、 名前被り時の解決方法は「普通の静的メソッドとして呼ぶ」という手段です。

一方、extension では、以下のように、キャスト的な文法で解決します。

int x = 0;

// 「暗黙」にやろうとすると、extension を使ったやり方でも解決不能・元々あるメソッド優先。
x.Method();   // これは解決不能。
x.ToString(); // これは int.ToString が呼ばれる。

// キャスト構文で解決可能。
((Ex1)x).Method();   // Ex1.Method。
((Ex2)x).Method();   // Ex2.Method。
((Ex2)x).ToString(); // Ex1.ToString。

// 「拡張型」の変数で1度受けるのでも解決可能。
// この場合は int のメソッドよりも extension のメソッドの方が優先。
Ex1 ex = x;
ex.Method();
ex.ToString();

implicit extension Ex1 for int
{
    public void Method(this int x) { }
    public void ToString(this int x) => x.ToString("X2");
}

implicit extension Ex2 for int
{
    public void Method(this int x) { }
}

実際に型として使える

Ex1 ex みたいな変数を定義できることからもわかる通り、 extension は普通に「型」という扱いです。 なので、拡張型 (extension types)と呼びます。

変数だけではなく、引数、型引数などにも使えます。

using System.Collections;

int x = 0;

// int → Ex1 の暗黙の変換。
M1(x);

// IEnumerable<int> → IEnumerable<Ex1> の暗黙の変換。
M2(new[] { 1, 2, 3 });

// 引数に拡張型を使う。
static void M1(Ex1 x) => Console.WriteLine(x);

// 型引数に拡張型を使う。
static void M2(IEnumerable<Ex1> x)
{
    foreach (var item in x) Console.WriteLine(item);
}

implicit extension Ex1 for int
{
}

explicit extension

これまで説明なしで implicit extension という書き方をしてきましたが、 そこから察していただける通り、explicit extension もあります。 名前通り型の明示が必須になって、 int などの元の型のままでメンバーを呼ぶことができなくなります。

// (implicit なら呼べるけど) explicit extension では呼べない。
1.Method();
int.StaticMethod();

// こんな風に、型を明示して呼ぶ想定。
Ex ex = 1;
ex.Method();
Ex.StaticMethod();

explicit extension Ex for int
{
    public void Method() { }
    public static void StaticMethod() { }
}

1.Method() みたな呼び方ができないものが『extension』なのか?」みたいな話はあります。 なので、元々は role, view, shape (同じデータの別の役割・見え方・輪郭)みたいな言葉を使おうかという話も出ていました。 ただ、変に用語を増やすよりは、「暗黙的拡張」、「明示的拡張」と呼び分ける方がいいのではないかということになって、こちらにも extension を使おうという流れになっています。

ちなみに、同じ型に対する別の extension はお互い型変換させるつもりはないそうです。

// 基となる型から extension への変換は暗黙 OK。
Ex1 ex1 = 1;
Ex2 ex2 = 2;

// extension 同士の変換はダメ。
Ex2 ex3 = ex1;

explicit extension Ex1 for int { }
explicit extension Ex2 for int { }

要は、strong-typedef 的なものに使えます。 (この辺りが「それは extension なのか?」と言われるゆえんです。 拡張するメンバーが一切なくても使い道があります。)

細かい文法話

extension は別の extension からの派生もOKで、 多重継承も認めるそうです。

インターフェイス実装もできるわけで、 : の後ろには他の extension とインターフェイスが並びます。 例えば以下のような感じ。 (T は通常の型、I 始まりのものがインターフェイス、X 始まりのものが extension。)

implicit extension X for T : XA, XB, IA, IB
{
}

ちなみに、ここでいう T (for の後ろの型)のことを「基になる型」(underlying type: 根底にある型、基礎となる型)と言います。 (C# 的には、enum なんかの enum E : int { } とかの int の部分も underlying type と言います。Microsoft の和訳では undelying type = 基になる型。)

クラスの場合は基底クラスとインターフェイスをあまり区別せず、class Derived : Base, IA, IB と書ける(ただし、基底クラスは先頭である必要あり)わけですが、 extension の場合は for を使って : とは分ける方向で考えているみたいです。 基底型をいくつも持てるし、ただでさえ基底型とインターフェイスの混在があるのに、さらに基になる型 T も並べた時に、「同じ : を使って、一番先頭という縛りを設ける」というのはいささか不安だったそうです。 特に、partial を認めるつもりなので、その場合に「一番先頭」があやふやになるのを懸念したみたいです。

implicit partial extension X for T : XA, IA
{
}

implicit partial extension X : XB, IB
{
}

また、既存の拡張メソッドがトップレベルの型での定義以外を認めていないのに対して、 新しい extension は入れ子を認めるそうです。

using static Ex;
using static C;

// ちゃんと呼べる。
1.M1();
2.M2();

implicit extension Ex for T
{
    implicit extension NextedEx for int
    {
        void M1() { }
    }
}

class C
{
    implicit extension NextedEx for int
    {
        void M2() { }
    }
}

さらに、ジェネリックにもできるそうです。

implicit extension X<T> for T : XA, IA
    where T : IT
{
}

派生 extension を作る際には、 基となる型の条件を強める方向でなら、基となる型の変更もできるみたいです。

implicit extension XBase for IEnumerable<object>
{
}

// IEnumerable<object> から IEnumerable<string> への変更はOK。
// (逆だとダメ。)
implicit extension XDerived1 for IEnumerable<string> : XBase
{
}

// ちなみに、基となる型に変更がないなら for は省略可。
implicit extension XDerived2 : XBase
{
}

実装方法

現状、文法面をどうするかが議論の中心で、 あんまり実装方法に関する決定はないみたいなんですが、 案として挙がっているのは以下のような方向性です。

例えば、前述の(以下に再掲) extension に対して、

implicit extension Ex for int
{
    public void Method() { }
    public int Property => int;
    public int this[int index] => index;

    public static void StaticMethod() { }
    public static Ex operator+ (Ex x) => x;
}

以下のようなラッパー構造体を作るのはどうかという案になっています。

ref struct Ex
{
    private ref int @this;
    public Ex(ref int @this) => this.@this = ref @this;

    public void Method() { }
    public int Property => @this;
    public int this[int index] => index;

    public static void StaticMethod() { }
    public static Ex operator +(Ex x) => x;
}

ref 構造体ref フィールドを使う想定なので、 別途以下のような機能(C# 11 時点で認められていない)が必要になります。

  • ref 構造体の ref フィールドを持てるようにする
  • ref 構造体をジェネリック型引数にする
  • ref 構造体でインターフェイスを実装する
// 現状、ref 構造体はインターフェイス実装を持てない。
ref struct S : IEnumerable<int>
{
    // 現状、ref 構造体の ref フィールドはダメ。
    ref S _refS;

    // 現状、ref 構造体を型引数に渡せない。
    IEnumerable<S> GetItems()
    {
        yield return default;
    }
}

実装フェーズ

冒頭に「C# 12 で全て実装されるかどうか怪しい」という話をしましたが、 具体的には以下のような3つのフェーズに分かれています。

  1. 静的メンバーの拡張だけ認める
  2. インスタンス メンバーも認める
  3. インターフェイス実装を認める

前節で説明したように、ref フィールドを使った実装にする可能性が濃厚なわけで、 これら3フェーズは要するに、

  • 静的メンバー: 現状でもできる
  • インスタンス メンバー: ref 構造体の ref フィールドを認めた上でやりたい
  • インターフェイス実装: ref 構造体のインターフェイス実装を認めた上でやりたい

という区分だったりします。

1と2を分けるのは少々気持ち悪いので実際にはこの2つは同時に提供されるかもしれませんが、 実装都合でいうと結構な難易度の隔たりがあるそうです。

ちなみに、「静的メソッドの拡張をしたい、既存の型に静的メソッドを追加したい」という要望もそれなりに昔からあるので、 1だけ先行実装というのもそこまで不自然でもないかもしれません。

C# での破壊的変更の今後の扱い(案)

$
0
0

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<BC>(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 でrequiredscopedfileが新たにキーワードになりました。

ただ、幸い、これらは(当然、文脈キーワードで)「型名として使おうとする時だけまずい」という仕様になっています。

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 では、そもそも requiredscopedfile という名前の型宣言自体エラーにしました。 結構な破壊的変更ですが、これで困ったという人は、僕の知る限りは見かけたことはありません。

(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 なところのコーディング規約は口うるさく言わない」みたいなところがあるので、フィールドに関しては _fieldthis.fieldfield の3つとも結構います。

さて、このラインの「大した影響ではないものの、無視できるほどは皆無じゃない」をどう扱いましょうか。 というのが現在の課題。

さかのぼって

field フィールド」程度の破壊的変更を認めたいのであれば、 過去のさかのぼれば、同程度の以下の影響範囲だけどもちょっと特殊対応して破壊的変更を避けたものがあります。

  • var: var という名前の型がないときに限りキーワード扱い
  • dynamic: dynamic という名前の型がないときに限りキーワード扱い
  • _: 1つも変数参照がないときに限り discard 扱い

特に前2者なんて、requiredscopde が型名として使えなくなった今、かなり不自然ですよね。

かつては「型推論の var を使わせないために、わざと class var {} を定義しておく」という嫌がらせのような規約を定めてしまう人も一部いたそうですが。 今では「そんなことやるのは推奨されていない」で一蹴していいと思います。

改めて、破壊的変更の影響軽減

とりあえず差し当たっては「field フィールド」問題、もしかするとさらに踏み込んで「var 型」問題を、今後、破壊的変更を認める方向で進めることになるかもしれません。

さすがにサイレントに行うには大きすぎる破壊的変更なので、以下のように進めたいとのこと。

  • コンパイラーを最新にした場合、言語バージョンを更新しなくても(TargetFramework を最新にしなくても)、「最新の C# で破壊的変更になる」旨を警告する
    • 言語バージョンを上げるつもりのない人向けに、抑止オプションも提供する
  • 自動コード修正を提供して、早期に修正してもらう
    • 半自動プロパティの例でいうと fieldthis.field に自動的に置き換える
  • このコード修正は IDE 上でも、コマンドラインでの実行もできるようにする

discussion での反応は「賛成多数」(👍100 対 👎4)。 むしろ、「他の言語はもっと破壊的変更してるだろ。やっちゃえ」発言も目立ちます。 ただ、この discussion に参加しに来る人はその時点で「積極的な人」のはずなので、 もう少しいろんな方面の調査は必要かと思われます。

また、1つ disucussion 内で挙げられた懸念として、 「StackOverflow とかからコピペしてくるコード問題」があります。 コード片のコピペの場合、「どのバージョンのコピーを、どのバージョンのコンパイラーにペーストするか」がわからないので、「事前に警告して、事前にコード修正をかけてもらう」戦略がやりにくいです。 (こういう問題も、top-level statemsntですでに経験済み。)

忘れがちなカルチャー依存問題

$
0
0

今日は、「Globalization Invariant Mode」に変更したら、意外と忘れがちなところで差が出たみたいな話。

Globalization Invariant Mode

以前に1回ブログに書いてるんですが、 .NET の文字列 API にはカルチャー依存なものが多くて、 例えば 1.2.ToString() すらカルチャー依存です。 大陸ヨーロッパだと小数点を , にすることが多く、そのあたりの OS でこの ToString を実行すると "1,2" になります。

一方で、カルチャーごとの書式情報みたいなのは結構データ量が多いので、 WebAssembly みたいなフットプリントを小さくしたい環境では「そのデータを除外したい」要件があったりします。

そこで導入されたのがGlobalization Invariant ModeCultureInfo.CurrentCulture を呼んでも、InvariantCulture が返ってくるというモードです。 1.2.ToString() も常に "1.2" に。

文字列比較

日本 (ja-JP カルチャー)の場合、InvariantCulture (実質 en-US カルチャー)との差は日付がらみ(米国は MM/dd/yyyy)くらいなので、 大した影響もないはず(フォーマット未指定で日付系の型を ToString することがあんまりない)なので早々に Globalization Invariant Mode を有効化しようとしたところ、 Order/OrderBy が影響を受けていました。

Globalization Invariant Mode の解説をよく見てみれば原因は明白で、

String operations like Compare, IndexOf and LastIndexOf are always performed as ordinal and not linguistic operations regardless of the string comparing options passed to the APIs.

文字列操作、例えば Compare, IndexOf, LastIndesOf などは常に ordinal で実行され、言語的な操作はしません。文字列比較のオプションに何を渡しても関係なく。

とのこと。

CurrentCultureInvariantCulture に化ける他に、 文字列比較が常に ordinal (文字コード順)になるそうです。

例えば以下のようなコードを実行すると、

Console.OutputEncoding = System.Text.Encoding.UTF8;

// InvariantGlobalization true のときはこの行が例外になるのでコメントアウトして実行。
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("tr-TR");

var data = new[]
{
    // だいたいのカルチャーで、大文字・小文字、アクセント記号違いの文字が並ぶようにソートされる。
    "a",
    "A",
    "b",
    "B",
    "À",
    // トルコ語の時に順序変わるやつ
    "i",
    "I",
    "ı",
    "İ",
};

var @default = data.Order().ToArray(); // 実行結果を見ればわかるものの、未指定は CurrentCulture。
var current = data.Order(StringComparer.CurrentCulture).ToArray();
var invariant = data.Order(StringComparer.InvariantCulture).ToArray();
var ordinal = data.Order(StringComparer.Ordinal).ToArray();

for (int i = 0; i < @default.Length; i++)
{
    Console.WriteLine($"{@default[i]} {current[i]} {invariant[i]} {ordinal[i]}");
}

InvariantGlobalization が false なら以下のような結果になります。 無指定が CurrentCulture と一緒。 CurrentCultureInvariantCulture では一部の並びに変化がありますが、 「大文字・小文字が並ぶ」くらいはどのカルチャーでも共通です。

a a a A
A A A B
À À À I
b b b a
B B B b
ı ı i i
I I I À
i i İ İ
İ İ ı ı

そして、InvariantGlobalization を true 以下のような結果に変化します。 無指定と CurrentCulture の列だけが変わるかと思いきや、 InvariantCulture の列も含めて全部が Ordinal とそろいます。 完全に文字コード順なので、ASCII アルファベットは大文字が先で、小文字がまとめて後ろに。 À は non-ASCII の文字なのでさらに後ろ。

A A A A
B B B B
I I I I
a a a a
b b b b
i i i i
À À À À
İ İ İ İ
ı ı ı ı

ちなみに、日本語でもひらがな・カタカナの並びが変わります。 (カルチャーあり: あ、ア、い、イ。 Ordinal: あ、い、ア、イ。)

実は遅い Order()

カルチャー依存のテーブルを引いて順序を決めるのと、 単に文字コードの数値を見てソートするのとではどちらが高速か明白です。

Order/OrderBy もカルチャー依存ということは… ベンチマークを取ってみましょう…

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<StringCompareBenchmark>();

public class StringCompareBenchmark
{
    private static readonly string[] _data = """
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
        """.Split(' ');

    [Benchmark]
    public string[] Order() => _data.Order().ToArray();

    [Benchmark]
    public string[] OrderCurrent() => _data.Order(StringComparer.CurrentCulture).ToArray();

    [Benchmark]
    public string[] OrderInvariant() => _data.Order(StringComparer.InvariantCulture).ToArray();

    [Benchmark]
    public string[] OrderOrdinal() => _data.Order(StringComparer.Ordinal).ToArray();
}

結果、うちの環境だと以下のような感じでした。

Method Mean Error StdDev
Order 8.348 us 0.0778 us 0.0727 us
OrderCurrent 8.399 us 0.0313 us 0.0277 us
OrderInvariant 8.453 us 0.0569 us 0.0532 us
OrderOrdinal 2.593 us 0.0125 us 0.0111 us

Ordinal 指定すると3倍以上速くなります。

ちなみに、自分の用途ではどこで何度実行しようと同じ順になりさえすればよくて、 順序は a A b B でも A B a b でもどちらでもよかったので、 もっと早くから Ordinal 指定しておくべき事案でした。

.NET の文字列比較でカルチャー未指定を検知する

$
0
0

先日の C# 配信で、 「これはブログに書いておくと助かる人がいるんじゃないか」と言われたものをブログ化。

背景: カルチャー依存問題再び

うちのブログでも何回か書いてるんですが、 .NET の文字列比較は、カルチャー依存比較するものと Ordinal (文字コード通り)比較するものが混在していて、なかなかにやばいです。

例えば以下のようなやつ。

using static System.Console;

// 正規化すると同じ文字になる、文字コード的には別の文字。
var s1 = "a\u0301"; // á = a + ́
var s2 = "\u00e1"; // á

// これは false。Ordinal 比較。
WriteLine(new Dictionary<string, int> { { s1, 0 } }.ContainsKey(s2));

// これは true。CurrentCulture 比較。
WriteLine(new SortedDictionary<string, int> { { s1, 0 } }.ContainsKey(s2));

なんでこんなことになるかというと、

  • DictionaryEqualityComparer 依存
    • EqualityComparerOrdinal 比較
    • Ordinal 比較だと、文字コード的に別の文字は一致しない
  • SortedDictionaryComparer 依存
    • ComparerCurrentCulture 比較
    • CurrentCulture 比較だと、たいていのカルチャーで a\u0301\u00e1 を同一視

という仕組み。

やばい。

前述のブログで一応の解決策として、 InvariantGlobalization モード指定してしまうという案も書いたんですが、 このモード変更は影響範囲が結構大きいので、 保守しているコードベースがでかいとなかなか踏み切れない方も多いと思います。

コード解析

このカルチャー依存文字列比較問題は .NET の中の人も把握していて、 .NET 5 の頃にいろいろと対策をしました。 その対策の1つに、NetAnalyzers の提供があります。

NetAnalyzers は要するに、「.NET SDK 付属の公式コード解析」です。 例えば Visual Studio からなら、Dependencis → Analyzers → Microsoft.CodeAnalysis.NetAnalysers のところで確認できます。

Visual Studio の Solution Exprorer で NetAnalyzers の内容を確認

この中で、カルチャー依存 API 対策になっているのは以下の項目。

  • CA1304: Specify CultureInfo
  • CA1305: Specify IFormatProvider
  • CA1307: Specify StringComparison for clarity
  • CA1310: Specify StringComparison for correctness

こいつら、デフォルトでは Silent なんですよね… (Silent = 何も表示しない。エラーや警告はおろか、サジェストのアイコンすら出ない。)

カルチャー依存 API のやばさのわりに Silent。 まあ、 .NET Framework 1.0 から .NET 5 までの十数年、ずっとそうでしたからね…

ということで、このコード解析の警告・エラー レベルを上げてしまった方がいいかもしれません。 .editorconfig に以下のような行を足せばエラーにできます。

[*.cs]
dotnet_diagnostic.CA1304.severity = error
dotnet_diagnostic.CA1305.severity = error
dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1310.severity = error

例えば以下のようなメソッドを警告にできます。

using System.Resources;

static string? M(ResourceManager m) => m.GetString(""); // CA1304
DateTime.Now.ToString(); // CA1305
"".IndexOf(' '); // CA1307
"abc".StartsWith("abc"); // CA1310

モジュール初期化子が呼ばれる順

$
0
0

前回のブログに続き、 先日の C# 配信で出てたネタ。

まあ、今回のは知ったところで誰が助かるということもないようなトリビア的な話です。

後だし優先で上書き

その C# 配信内で、 「CultureInfo.DefaultThreadCurrentCulture を上書きすれば対処はできるけども」 みたいな話が出まして。

ただ、まあ、こういう「グローバルに影響がある静的プロパティの書き換え」は決してお行儀はよくないじゃないですか。 誰でも、いつでも上書き可能。 すぐに競合しかねません。

例えばここで出した DefaultThreadCurrentCulture だと、 人それぞれ、以下のようなバラバラの主張が混ざったとします。

  • 常に InvariantCulture にしたい
  • 常に ja-jp カルチャーにしたい
  • 基本、InvariantCulture でいいものの、日付の書式だけは MM-dd-yyyy が許せないので上書きする

そしてしかも、これを「ソース ジェネレーターとかを使って裏でこっそり書き換えておきたい」とかやったときに、誰の主張が通ってしまうでしょうという問題。

using System.Globalization;
using System.Runtime.CompilerServices;

// file: A.cs
class Aさんの主張
{
    [ModuleInitializer]
    public static void Init()
    {
        CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
    }
}

// file: B.cs
class Bさんの主張
{
    [ModuleInitializer]
    public static void Init()
    {
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("ja-JP");
    }
}

// file: C.cs
class Cさんの主張
{
    [ModuleInitializer]
    public static void Init()
    {
        var c = (CultureInfo)CultureInfo.InvariantCulture.Clone();
        c.DateTimeFormat.LongDatePattern = "yyyy'-'MM'-'dd";
        c.DateTimeFormat.LongTimePattern = "HH':'mm':'ss";
        c.DateTimeFormat.MonthDayPattern = "MM'-'dd";
        c.DateTimeFormat.YearMonthPattern = "yyyy'-'MM";
        c.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd";
        c.DateTimeFormat.ShortTimePattern = "HH':'mm':'ss";
        CultureInfo.DefaultThreadCurrentCulture = c;
    }
}

そしてこの話題から、「後だし優先といわれても、 じゃあ、モジュール初期化子の実行順序は決まっているの?」という話題になります。

呼び出し順序

モジュール初期化子の実装方法」のところで書いてるんですが、 C# 的に「1つのモジュールに複数のモジュール初期化子がある」とき、 コンパイル結果的には「本当のモジュール初期化子は1つで、その中で複数のメソッドを呼ぶ」みたいな実装になっています。 問題はこの「複数のメソッド呼び出し」がどういう順序で並ぶか。

C# の仕様書上は「reserved, but deterministic order」 (詳細は明言せず将来変更の余地を残す、ただし決定論的な順序) となっているみたいです。 こういう場合、同じ環境(同じツールの同じバージョン)でコンパイルする限りは、 同じソースコードからは同じ実行ファイルができます。 その一方で、環境が変わると結果が変わる可能性あり。

ここでは現時点での実装がどうなっているかという話もしておきましょう。

同じファイルの中

まずは同じファイルの中に複数のモジュール初期化子を書いた場合。 これはまあ結構単純で、上から順番です。

メタデータ的に、同じ名前空間内のクラスは書いた順に並ぶし、 クラスのメンバーも書いた順に並びます。 モジュール初期化子はその順で呼ばれます。

例えば以下のようなコードを1つのファイルに書いた場合、

using System.Runtime.CompilerServices;

class Z
{
    [ModuleInitializer]
    public static void Init1() { }

    [ModuleInitializer]
    public static void Init2() { }
}

class Y
{
    [ModuleInitializer]
    public static void Init2() { }

    [ModuleInitializer]
    public static void Init1() { }
}

class A
{
    [ModuleInitializer]
    public static void Init() { }
}

呼ばれるのは Z.Init1, Z.Init2, Y.Init2, Y.Init1, A.Init の順です。

ファイルが分かれている場合

問題は複数のファイルが分かれている場合。

調べた感じ<Compile> タグの手書きで順序を明示することもできるけども、 *.cs 指定にした場合はビルド ツール依存 (MSBuild.exe と dotnet build で微妙に結果に差あり) みたいです。

とはいえ、まあ、おおむね、ファイル名でソートしているみたいです。 補足として、

  • Ordinal 比較でソート
    • 安心してください、カルチャーには依存しません
    • a と b の間に À が来たりしません。文字コード順
  • MSBuild.exe は UTF-16 比較、dotnet build は UTF-8 比較してそう

となります。

例えば以下のような2つのファイルを同じプロジェクトに含めた場合、

// このコードを A.cs に書く
using System.Runtime.CompilerServices;

class Z
{
    [ModuleInitializer]
    public static void Init() { }
}
// このコードを Z.cs に書く
using System.Runtime.CompilerServices;

class A
{
    [ModuleInitializer]
    public static void Init() { }
}

(型名とファイル名が逆なことに注意。)

呼ばれるのは Z.Init, A.Init の順です。

一番最後に並ぶもの

ここで「後だし優先」の話に戻ります。

これまでに話した仕様に沿って、極力「後だし」になるためにやれそうな工夫としては、

  • 文字コード的に後ろの方の文字、例えば半角カナとかのファイル名を付ける
  • <Compile Include=""/>タグを明示的に後ろの方に書く
  • .targets ファイル(.csproj の末尾に import されるファイル)に <Compile> タグを書く

とかですかね。

まあ、モジュール初期化子の実行順序に依存するようなコードを書くのは邪悪なのであんまりおすすめはしませんが。


Length-based switch dispatch

$
0
0

「そういやブログに書いてなかった」ネタ。 Pull Request が通った プレビュー版(Visual Studio 16.6 Preview 1)でよければ今年の2月頃から使えてた話です。

文字列に対する switch に新しい最適化手法が導入されました。

元々の switch のコスト

例として以下のような switch を考えます。

static int StringSwitch(string s) => s switch
{
    "abc" => 0,
    "def" => 1,
    "ghi" => 2,
    "01234a" => 3,
    "01234b" => 4,
    "01234c" => 5,
    "aaaaaaaa" => 6,
    _ => -1,
};

C# コンパイラー的には、

  • case 少なければ単に上から順に if (s == "...") を並べる
  • 多ければ IL の switch 命令を出力

みたいな処理をしていました。

ちなみに、IL の switch 命令は、

  1. case 文字列に対応するハッシュ値を事前計算
  2. s.GetHashCode()switch する

みたいなコードを生成するそうです。

if (s == "...") を並べる方式はワーストケースでは多数の == 比較がかかりますし、 文字列に対する GetHashCode の計算は意外と重たい処理です。

新手法 switch

Visual Studio 17.6 (Roslyn 4.6) 以降では、case の数が中程度のとき、 以下のような分岐をかけるようになりました (Trie木的発想の簡易アルゴリズム)。

  1. 文字列長でまず分岐
  2. どこか1文字だけを使って charswitch
  3. その後に string== 判定

Length-based switch dispatch (文字列長ベースの switch 分配)というそうです。

先ほどの例の switch だと、おおむね以下のような感じの分岐に置き換わります。 (実際はもうちょっと goto だらけのコードになりますが、 見やすさ優先で変更。)

StringSwitch("");

static int StringSwitch(string s) => s.Length switch
{
    3 => s[0] switch
    {
        'a' => s == "abc" ? 0 : -1,
        'd' => s == "def" ? 1 : -1,
        'g' => s == "ghi" ? 2 : -1,
        _ => -1,
    },
    6 => s[5] switch
    {
        'a' => s == "01234a" ? 3 : -1,
        'b' => s == "01234b" ? 4 : -1,
        'c' => s == "01234c" ? 5 : -1,
        _ => -1,
    },
    8 => s == "aaaaaaaa" ? 6 : -1,
    _ => -1,
};

これで、どの case にも当たらないときには「長さ比較 + 1文字比較」で終わり、 当たった時でもそれに加えて少数の文字列 == になります。

例えば .NET の中の人が HTTP の content type 用の分岐で測った感じだと、 5~10倍くらい速いみたいです。

C# 13 向けトリアージ

$
0
0

.NET 8 も RC 2 な段階になって、ここから GA までの間に仕様が変わるということはほとんどなくなってきました。 となると、話題はもうその次。来年向け(C# 13 / .NET 9 ターゲット)の話が出てきます。 C# Design Meeting でも、13向けのトリアージがちらほら始まりました。

とりあえず現状、2件。

以下のようなものがトリアージされました。

10/9 議事録

ReadOnlySpan initialization from static data 5295

#5295

C# 7.2 辺りから、以下のような「配列のアロケーションを消す」最適化が掛かります。

// 定数だけで構成された byte 配列は最適化で消える。
// new ReadOnlySpan<byte>(静的データのポインター, 4) みたいなコードに展開される。
ReadOnlySpan<byte> data1 = new byte[] { 1, 2, 3, 4 };

// .NET 7 までは byte, sbyte のみだったけど、 .NET 8 からはそれ以外の整数にも最適化がかかるように。
ReadOnlySpan<int> data2 = new int[] { 1, 2, 3, 4 };

けども、見た目はどう見ても配列を作っているので、たびたび「この配列を new するのもったいなくない?」という突っ込みが入りがちです。

そこで、以下のような「ReadOnlySpan の初期化構文が欲しい」という話がありました。

ReadOnlySpan<byte> data1 = { 1, 2, 3, 4 };

ReadOnlySpan<int> data2 = { 1, 2, 3, 4 };

ですが、C# 12 で入る予定のコレクション式がこれを兼ねるので、この {} を使った書き方はリジェクトになりました。

Embedded Language Indicators for raw string literals 6247

#6247

// こんな風に、raw string の先頭行に「文字列リテラルの中身が何か」を示すインジケーターを書きたいという案。
var y = """regex
    \s+
    """;

// ちなみに今も、以下のように「文字列リテラル直前のコメントに lang = を付ける」という手段でインジケーターを書ける。
// Visual Studio はこれを認識して色付けしたり補間したりしてくれる。

// lang=regex
var y = """
    \s+
    """;

優先度付くほど強いモチベーションがなさげ。 Backlog (過去ログ行き)。

list-patterns on enumerables

#6574

IEnumerable に対して x is [] とか書けるようにしたいというやつ。

時間がなくて12でも入らなかっただけ。 Working set (作業中)。

Make generated Program` for top-level statements public by default

#6769

トップ レベル ステートメントから生成される Program クラスを public にしたいという話。

一番のモチベはテストだけど。 テスト関連、もっと広く要件調査必要。

CallerCharacterNumberAttribute

#3992

Caller Info 属性に追加で「ソースコードの何列目か」を取れるものを足したいという話。 (今、CallerLineNumber で行番号は取れるけども、列を取る手段がない。)

Interceptorと一環としてやる。

Add private and namespace accessibility modifiers for top-level types

#6794

「同一の名前空間内限定でアクセスできる」というアクセスレベルの新設。 file は狭すぎるし、internal は広すぎる。

やる気になってるっぽい(Working set)。

Require await to apply nullable postconditions to task-returning calls

#6888

非同期メソッドが絡んだ時に MemberNotNull` とかがちゃんと働かない問題。

作業中。もらったフィードバックに対処が必要。

is expression evaluating const expression should be considered constant

#6926

const int x = 123;
const bool y = x == 0; // これは OK。const 同士に対する式の結果は const。
const bool z = x is 0; // 今ダメ。 == が行けるんなら is も行けていいんじゃない?

Any time (C# チーム内ではやらないけど、コミュニティ貢献受付はできる)。 実際、コミュニティ実装が始まってそう

10/16 議事録

Breaking change warnings

#7189

「C# 14 で破壊的変更になる予定だから注意してね」警告みたいなやつを C# 13 以下に対して出そうかという話。 (主に、field キーワード導入がモチベ。)

これについて書いたブログ: C# での破壊的変更の今後の扱い(案)

普通に作業中。 10/9 のミーティングでは取り上げ忘れてただけ。 Working set。

Determine natural type of method group by looking scope-by-scope

#7364

#7429 との重複扱いで close。

u8 string interpolation

#7072

$"直接 UTF-8 で書き込まれる文字列補間 {x} {y}"u8 (u8 接尾辞) みたいなのが欲しいという話は上がってたんだけど。

.NET 8 の並々ならぬ努力の結果、JIT 最適化がだいぶ賢くなった。

using System.Text.Unicode;

int x = 123;
int y = 456;
Span<byte> dest = stackalloc byte[100];

Utf8.TryWrite(dest, $"UTF-8 補間 {x} {y}", out var written);
// ↑ 普通の(UTF-16 な)文字列補間だけど、JIT の努力によって UTF-16 → UTF-8 への変換がほぼノーコストに最適化される。

その結果、 $""u8 の要求減った。 Backlog 行き。

Lock statement pattern

#7104

lock (obj) もパターンベースにしたいという話。

.NET の「任意の object を lock に使える」、「オブジェクトヘッダーに lock 用の syncblock って領域を持ってる」という仕様、オーバーヘッドが大きいので、ちゃんと Lock` 型みたいなのを用意してそれを使って lock したい。

この Lock 型インスタンスに対して lock (_lock) されたときに、syncblock 使わず、パターンベースで Lock.TryEnter が呼ばれるようにしたい。

.NET 9 マイルストーンで Working set に。

String/Character escape sequence \e as a short-hand for \u001b ()

#7400

エスケープ文字(U+001B)に対するエスケープシーケンス \e を導入したい。

Any time に(今、提案者に対してコミュニティ実装するか聞いてるところ)。

New operator %% for canonical Modulus operations

#7599

C# の % 演算子 (というか、大体の CPU の div rem 命令) は、オペランドの符号によっては 0~n-1 にならない。 それに対して、array[x % array.Length] みたいな用途では 0~n-1 になってほしい。

という、需要はあるものの、C# 言語組み込みでやるべきかどうかは疑問。 div rem にはいろいろ種類があるんで、メソッド名とかメソッド引数で「どの div か」を明示すべきという話に支持が集まってる。

なので C# としてはやらない(likely never)。

代わりにライブラリ追加の提案が runtime の方で進みそう (#93568)。 ↓提案内容。

namespace System.Numerics;

public enum DivisionRounding
{
    Truncate = 0,        // Towards Zero
    Floor = 1,           // Towards -Infinity
    Ceiling = 2,         // Towards +Infinity
    AwayFromZero = 3,    // Away from Zero
    Euclidean = 4,       // floor(x / abs(n)) * sign(n)
}

public partial interface IBinaryInteger<TSelf>
{
    // Existing:
    static virtual (TSelf Quotient, TSelf Remainder) DivRem(TSelf left, TSelf right);

    // Proposed:
    static virtual TSelf Divide(TSelf left, TSelf right, DivisionRounding mode);
    static virtual (TSelf Quotient, TSelf Remainder) DivRem(TSelf left, TSelf right, DivisionRounding mode);
    static virtual TSelf Remainder(TSelf left, TSelf right, DivisionRounding mode);
}

\e (エスケープ文字のエスケープ シーケンス)

$
0
0

先々月書いた「C# 13 向けトリアージ」で紹介してた C# 13 候補の1つ、「\e エスケープ シーケンス」が早々に実装されてたという話です。

.NET 8 正式リリース記念の配信ではちょこっと触れてたんですが、そういえばブログには書いてなかったので紹介。

エスケープ文字

キーボードで打てないような文字や、画面に表示されない文字を入力したりするために、 「\n と書いたら改行(U+000A, new line, line feed)に置き換える」みたいな仕様があり、これをエスケープ シーケンス(escape sequence, 回避用の一連の文字列)と言います。

C# をはじめ、C 言語の影響を受けて作られた言語の多くは \ (reverse solidus, 逆スラッシュ)で始まる文字列によってエスケープします。 プログラミング言語だと他には ` (逆引用符、グレイブ アクセント, grave accent)とかを使うものがあったりしますが、 要は、利用頻度があまりない文字をエスケープ シーケンスの開始文字にすることが多いです。

一方で、ASCII コードには古よりずっと、エスケープ文字(U+001B, escape character)というものがあります。 名前通りエスケープ シーケンスの開始文字として使われるもので、 多くのターミナル アプリがこのエスケープ文字を使ったシーケンスに対応しています。 ANSI (American National Standards Institute) によって策定された標準仕様があって、大体のターミナルはこの仕様に基づいた実装を持っています。 (この仕様は ANSI X3.64 というそうです。)

例えば C# で以下のようなコードを書いて実行すると、たいていの環境で赤い文字が表示されるはずです。

Console.WriteLine("\u001b[31mred text");

\u001b がエスケープ文字(以下、ESC と表記)で、ESC + [31m という文字列を Console に書き込むとそれ以降の文字色が変わります。

エスケープのエスケープ

そして C# 13 候補として、このエスケープ文字(U+001B)に対する C# のエスケープ シーケンスとして、\e が提案・承認されました。

C# 12 以前でも \x + 16進数2桁とか、\u + 16進数4桁とか、 \U + 16進数8桁とか、 任意の文字コードを直接打ち込むエスケープ手段があったので、別にそれほどなくて困るものでもなかったりはします。 以下のコードの \x1b, \u001b, \U0000001b はいずれもエスケープ文字です。

Console.WriteLine("\x1b[31mred text");
Console.WriteLine("\u001b[4munderlined text");
Console.WriteLine("\U0000001b[0mreset style");

古からある仕様ですが、 長らく C# の主戦場だった Windows では 「文字のスタイル変更は Console.ForegroundColor などの API 経由で行ってほしい」 みたいな感じで、あまり ANSI X3.64 を利用する文化ではありませんでした。

しかし最近は Linux 上での C# 利用も増え、 Windows も今時っぽい新しいターミナルを搭載するようになり、 ANSI X3.64 を積極的に使いたいという要望がちらほら増えてきました。

また、 Windows Terminal が新しくなった今となっては、 Console.ForegroundColor などの .NET の API を使って操作できるものよりも、 ANSI X3.64 でやれることの方が多くなっていたりします。

そこで出てきたのが \e でエスケープ文字を表せるようにしてほしいという要望。

\e 提案の検討

この提案で得られるメリットや、かかるコストを考えてみましょう。

まずメリットの方。 前節で書いた通り、エスケープ文字を使いたいことはちらほらないこともなく、「あれば便利かも」とは思います。 とはいえ、毎回自分で ANSI X3.64 を書くかと言われると微妙。 「31番が赤」とかいちいち覚えないですからね。 C# でも、ANSI X3.64 出力用のライブラリを提供してくれている方がいらっしゃいます: Kokuban (安定の Cysharp)。

また、元から \x1b と書けたわけで、「\e と書けるようになって楽かどうか」と言われるとたった2文字の短縮です。 もちろん、「エスケープ文字の文字コードは何だったっけ?」というのを覚えるよりは「エスケープの頭文字をとって e」というのの方が覚えやすそうではあります。

コストに関しては、エスケープ シーケンスの解析用の switch ステートメントに1個 case を追加するだけです。 以下のたった3行の追加。

    case 'e':
        ch = '\u001b';
        break;

「C# 12 以下では使えない」みたいな判定を足すとしてもさらに追加で +3 行。 テストとかを足しても数百行程度の修正になります。 ここの case 1個くらいならコンパイル実行時のコストもほとんどなし。

要するに、割かし毒にも薬にもならない、低コスト低リターンな提案ということになります。

なので、C# チームによる判定は「Any Time」。 この「Any Time」は、

  • 自分たちで実装の労力は割かない
  • コミュニティによる Pull Request が来た場合は受け付ける

みたいな温度感です。

そして実装

「Any Time」のわりにもうすでに実装されたものがあるわけですが。 以下のコード、Visual Studio 17.9 Preview 1 (11月15日にリリース) で動きます。

Console.WriteLine("\e[31mred text");
Console.WriteLine("\e[4munderlined text");
Console.WriteLine("\e[0mreset style");

\e もう動いてる

普通、コミュニティ実装だとそこそこ時間がかかるんですけどね。 何せ、「専業でやっているわけじゃない外部の人のコードのレビュー」みたいなプロセスを経るので。

\e に関しては、

という感じ。 「Any Time」とは…

Pull Request を作った方、C# チームの人ですしね。 定時後とかにさらっとやっちゃった感じかなぁと。 ホリデーの飛行機の中で embedded language を実装しちゃうような人なので。

ということで、「.NET 9 のプレビュー版もまだ出てないのにもう C# 13 候補機能の1つが実装されてリリースされてる」という面白状況に。

オブジェクト初期化子中の ^ 演算子

$
0
0

今日の C# 話はちょこっとした修正の話になります。 これまで new C { [^1] = 1 }; がコンパイル通らなかったみたいで、これが最近修正されました。

(Visual Studio 17.9 Preview 3 (1月17日リリース済み)の時点で実装されていました。 気づいてはいたけども、小さすぎてブログにするかどうか迷ってるうちに3週間ほど経過。)

以下のコードで示すような修正内容です。

// これがコンパイル エラーを起こす。
// (Visual Studio 17.9 Preview 3 以降を使うとコンパイルできるようになった。)
var c = new C { [^1] = 1 };

// これなら昔からコンパイル通る。
// (オブジェクト初期化子はこれと同じコードに展開されるはずなのに。)
c[^1] = 1;

class C
{
    // インデクサーと Length さえ持っていれば c[^i] と書けるようになる。
    // c[c.Length - i] 扱い。
    public int Length => 1;
    public int this[int i] { get => i; set { } }
}

まあ、^ を導入した時にオブジェクト初期化子は考慮漏れしてたんですかね。

こんなのでも一応悩むポイントはありまして。 1つは、例えば入れ子で new C() { [^1] = { [2] = 42, [3] = 43 } } とか書いたとき、

// 2行に分かれる = Length - 1 の計算が2度走る。
var c = new C();
c[^1][2] = 42;
c[^1][3] = 43;

// ^ の結果をキャッシュする。
var c = new C();
var cachedIndexArgument = ^1;
c[cachedIndexArgument][2] = 42;
c[cachedIndexArgument][3] = 43;

か、どちらがいいかという問題。

もう1つ、new C() { [^1] = { } } みたいに入れ子の部分が空っぽの場合、Length を評価する必要はあるかどうかとかも。

Length が副作用を持っている」とか「c[^1] が副作用で Length を書き換える」みたいな変なことをしているとこの辺りの結果が変わるわけで。

結局、以下のような選択をしたそうです。

  • 前者は、「^ の結果をキャッシュする」の方を選択
  • 後者は、[^1] = { } の時は評価しない(Length - 1 の計算自体せず、Length の getter は呼ばない) を選択

おまけ: ちょっと予告

あと、ちょこっと次回以降の予告。

しばらくブログ化していませんでしが C# 13 向けの作業がちらほら。 特に、2月に入ったくらいからアクティブで、結構検討が進んでいるみたいです。

最近見かけている話題を見出しだけ出しておくと以下の通り。

  • コレクション式の改善
  • ジェネリクスで ref struct を使えるように
  • Extensions
  • ジェネリクスの部分型推論 _
  • partial プロパティ
  • 破壊的変更がらみ

しばらくネタをため込んでしまったために大量に… 一気に書くと大変なので、次回以降、1個ずつブログにしようかと思います。

C# での破壊的変更の今後の扱い (続報)

$
0
0

去年の3月にブログに書いたものの続報。

C# でも限定的に破壊的変更を許していこうかという話だったわけですが、 ちょっと具体化しました。

ある機能を実現するにあたって破壊的変更の原則と進め方についての話をしています。

破壊的変更の候補

C# 13 で導入したい field アクセス(自動プロパティのバッキングフィールドにアクセスするための field キーワード)と、 これまでに破壊的変更を避けるためにちょっと変な設計になっている var (型推論変数宣言)、_ (discard)が検討の対象になっています。

破壊的変更を認める基準

  1. あくまで控えめな破壊的変更で、エンドユーザーに明確なメリットがある
  2. 破壊的変更を踏むようなコードは割かしレア
  3. 破壊的変更を起こす予定のコードはどういう理由でどこが問題で、どう直せばいいかが明確に示せる
  4. 破壊的変更を避けられるよう、完全に自動で、簡単で、堅牢で、局所的な code-fix が提供できる

field の場合

field アクセスは以下のような話。

class こういうのを
{
    private int _x;
    public int X
    {
        get => _x; set => _x = int.Min(value, 0);
    }
}

class こう書きたい // C# 13 候補
{
    public int X
    {
        get => field; set => field = int.Min(value, 0);
    }
}

class こういうコードで困る
{
    private int field;
    // ↑「このフィールドがないときだけ field をキーワード扱いする」みたいなことすると使い勝手が悪くなる。

    public int X
    {
        get => field; set => field = int.Min(value, 0);
    }
}

これは以下のように、前述の基準を満たします。

  1. field アクセスが欲しいという要望は多い。「field フィールドがないときだけ」とやると構文が複雑になるし、使い勝手も悪くなる
  2. field という名前のフィールド」はなくはないだろうけども多くはないし、問題になるのはプロパティのアクセサー内だけ
  3. field が将来キーワードになる」(から使うな)という明確な説明ができる
  4. 型名や this を付けて A.field とか this.field と書くように変えればいい

var の場合

C# 3.0 の頃からある varですが、 有名な話、「class var { } とかいう型をどこかに書いておけば、型推論の var を阻害できる」という問題があります。

// 普通は型推論の var になるはず。
var x = 1;

// が、こういうことをすると var x の意味が変わってしまう。
class var
{
    public static implicit operator var(int _) => 0;
}

嫌がらせでしかないんですが、 昔は「型推論とか怖いから嫌がらせしてやれ」と言っちゃう人が実際いたとか…

今でも「型推論は嫌」ということをもう人はいるとは思いますが、 その場合も今はソースコード分析の設定を変えて警告なりエラーにできるようになっているので「class var { }」みたいな変なことをする必要はありません。

なので、もう今となってはこれも破壊的変更してでも「var は常に型推論」にしてしまっていいのではないかという話になります。

これについての前述の基準:

  1. class var { }みたいなものは実用的じゃない。その割に var を常にキーワード扱いできないのは構文ハイライトとかで結構困る
  2. 嫌がらせ以外で「class var { }」を書く人もいない
  3. var という名前の型は作るな」と説明できる
  4. もしどうしても「var 型」を作りたければ @var と書けばいい

_ の場合

C# 7 で discard が導入されたわけですが、 これも「_ を普通に変数として使っていないときに限り、_ が discard の意味になる」という挙動になっています。

void m1(int i, string s)
{
    // これはいずれも discard。
    (_, string _) = (i, s);
    int.TryParse(s, out _);
}

void m2(int i, string s)
{
    var _ = i; // これがあるせいで…

    (_, string _) = (i, s); // ここの1個目の _ は変数。
    int.TryParse(s, out _); // ここの _ は変数。
}

void m3(int i, string s)
{
    var _ = i;
    var _ = s; // これは「同じ名前の変数がすでにある」エラー。

    (_, string _) = (i, s);
    int.TryParse(s, out _);
}

これについての前述の基準:

  1. 今のままだといつ _ が discard になるかわかりにくすぎる
  2. 元々 _ を変数・引数として使っていた人も、「値を特に読まない」(なのでほんとは discard にしたい)という意味でこの名前を使うことが多い
  3. _ が常に discard の意味になる」と説明できる
  4. @_ と書けば「_ という名前の変数」を書ける

破壊的変更の影響を軽減

破壊的変更に対応しやすくするため、 C# N に対応したコンパイラーを使ったとき、 まだ C# N - 1 以下だった場合に警告と code-fix を提供したいとのこと。

現在、LangVersion を明示しなかった場合、 .NET SDK が「TargetFramework に応じた言語バージョンを自動選択する」という挙動になっています。

なので、例えば以下のような流れで比較的安全にバージョンアップができます。

  • .NET 9 SDK をインストールすると C# 13 対応コンパイラーになる
  • この時点で既存のプロジェクトは net8.0 とかがターゲットになっているはずで、C# 12 が選ばれる
  • 「C# 13 対応コンパイラーで C# 12 を利用」状態なので、field に関する警告が出る
  • 警告を直してから net9.0 ターゲットに上げると安全にバージョンアップができる

ただ、この手の警告の追加自体が破壊的変更 (警告は必ず取る方針であったり、なんなら WarnAsError オプションでエラーにできる)なので、 年に1回のメジャーバージョンアップ時以外には警告追加しないとのこと。

C# のバージョンアップを予定していない人向けに警告抑止の手段の提供や、 もしかしたら「先送りはするけどいつかバージョンアップしたい」人向けに「30日だけ警告を止める」みたいな手段を提供するのがいいかもしれないという話も出ています。

LangVersion latest, preview

LangVersion latest にすると、 常に C# コンパイラーが対応している最新の C# バージョンになります。 こうなると先ほどの「C# N 対応コンパイラーで C# N - 1」という状態が起きなくなるので、 「言語バージョンアップ前に破壊的変更を修正」ということができません。 なので、latest は今後非推奨にして、使っていたら警告を出すことを検討しているそうです。

一方で、LangVersion preview はわかってて使っている人柱向けですし、 プレビュー提供している言語機能は普通にリリースまでに破壊的変更がかかることもあって、 元から破壊的変更は覚悟の上で使っているはずです。 なので、preview に対しては特に問題視はしないそうです。

Viewing all 483 articles
Browse latest View live