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

ref 構造体のインターフェイス実装 / 型引数での使用

$
0
0

ref 構造体で説明しているように、 Span<T> 型など一部の型は「スタック上にないといけない」という強い制約があります。

この制約を守るため、これまで、ref 構造体は

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

という制限が掛かっていました。

C# 13 では、この制限を緩和するため、 ジェネリック型引数に「allows ref struct」という「アンチ制約」を追加する予定です。

こういう案自体は ref フィールドが追加された C# 11 (2022年)の頃から温められてはいたんですが、 いよいよ C# 13 で本格的に取り組むみたいです。 .NET 8/C# 12 がリリースされた後くらいからちらほら提案ドキュメントの更新あり。

ちなみに、ランタイム側はその2022年頃に対応すでに入っているみたいです。

ref 構造体の制限緩和の要求

わかりやすい例でいうと、Span<T>IEnumerable<T> であってほしいというものです。 C# 12 時点だと、以下のような2重実装を余儀なくされています。

List<int> list = [1, 2, 3, 4, 5];
ReadOnlySpan<int> span = [1, 2, 3, 4, 5];

Console.WriteLine(MyMath.Sum(list));
Console.WriteLine(MyMath.Sum(span));

static class MyMath
{
    public static int Sum(IEnumerable<int> numbers)
    {
        var sum = 0;
        foreach (var x in numbers) sum+= x;
        return sum;
    }

    // メソッドの中身全く同じ。
    // Span/ReadOnlySpan が IEnumerable じゃないので別メソッドでの実装が必須。
    public static int Sum(ReadOnlySpan<int> numbers)
    {
        // 実装的に、numbers をボックス化したり、ref フィールドを外に漏らしたりもしてない。
        // IEnumerable に対する実装をそのまま使って何も問題ない。
        var sum = 0;
        foreach (var x in numbers) sum += x;
        return sum;
    }
}

ref 構造体にインターフェイス実装を持たせること自体はそこまで問題ではありません。 問題は、以下のように、「インターフェイス型の変数に直接代入してしまうとボックス化を起こしてまずい」という点です。

Span<int> span = [1, 2, 3, 4, 5];

// たとえ、Span が IEnumerable<T> を実装していたとしても、
// 以下のようなコードを書くとこの時点でボックス化が起きる。
// span がヒープに漏れてしまうのでまずい。
IEnumerable<int> e = span;

じゃあどうすべきかというと、ジェネリクスを介します。

Span<int> span = [1, 2, 3, 4, 5];

// ジェネリクスを介すれば、ボックス化を起こさずにインターフェイスのメンバーを呼べる。
// (前述の問題はクリア。)
static T Sum<T, TEnumerable>(TEnumerable list)
    where TEnumerable : IEnumerable<T>
{
    // 省略
    return default!; // 仮
}

// なので残る問題はこっち。
// ref 構造体を型引数に渡したい。
Sum<int, Span<int>>(span);

ということで次節で説明する「アンチ制約」が必要になります。

アンチ制約

ジェネリック型制約(where T : みたいなやつ)は、普通、制限を掛けることで、

  • メソッド内で Tに対して できること(呼べるメソッドとか)が増える
  • その代わり、呼び出し側で T に対して渡せる型が減る

というものになります。

// 制限なし。
static void M1<T>() { }

// 何の型でも渡せる。
M1<int>();
M1<string>();
M1<object>();

// 制限あり。
static void M2<T>() where T:ISpanParsable<T>
{
    // 呼べるメソッドが増える。
    T value = T.Parse("123", null);
}

// 渡せる型が減る。
M2<int>();
M2<string>();
M2<object>(); // コンパイルエラー。

ところが今回、「ref 構造体を渡せるようにしたい」という逆の要件なので、「制約」ではなく「アンチ制約(制約の撤回)」が必要になります。

2年くらい前のブログでちょこっと触れていますが、 逆のことをするのに where T : ref struct とは書かせたくないようで、ちょっと別文法を模索していました。 当初案だと allow T : ref struct とかも検討されていたんですが、 結局は where T : allows ref struct (where はそのまま。制約の前に allows)になりそうです。

// allows で制限を緩める。
static void M3<T>(T x)
    where T : allows ref struct // アンチ制約。
{
    // メソッド内でできることが減る。
    object obj = x; // box 化ダメ。エラーにする予定。
}

// 渡せる型が増える。
M3<int>();
M3<string>();
M3<object>();
M3<Span<int>>(); // allows ref struct がないと呼べない。

ちなみに、where T : IDisposable, allows ref struct みたいに、制約とアンチ制約は並べて書けます。


インターセプター

$
0
0

「最近動きがあったもの」ブログをいくつか書いてて、 「続報」みたいなものも書いてるわけですが。

今日のも「まあ、去年から動く実装すでにあるんだけど」という意味では続報なものの、 今日のインターセプターはあまりうちのサイトで取り上げておらず、初めて説明を書く話。 (ライブ配信では時々話に出てるんですが。)

インターセプター

今日話すインターセプターは、まあ、Source Generator向けの機能です。

既存の Source Generator でも、 クラスを丸ごと生成するとか、メソッドの中身を生成とかはできます。

.NET が標準で提供しているやつだと GeneratedRegex とか。 partial メソッドに属性を付けて、メソッドの中身をコード生成しています。

partial class Reg
{
    // この属性を付けた partial メソッドに対して、
    // Sytem.Text.RegularExpressions.Generator でコード生成してる。
    [GeneratedRegex(@"\d+")]
    public static partial Regex Digits();
}

これで困るのは、メソッドの呼び出し箇所ごとに違う実装をコード生成したい場合です。

一例として以下のようなコードを考えます。 要は「const string を Parse するならコンパイル時に全部やっちゃえるのでは」という話。

using static System.Console;

// const string の Parse はコンパイル時にできるのでは。
WriteLine(int.Parse("123"));
WriteLine(int.Parse("456"));

// こういうのは無理として。
WriteLine(int.Parse(ReadLine()!));

int だと「最初から 123 と書け」と言われればそれまでなので役に立ちませんが、 例えば BigInteger とかなら意味がありそうです。

こういう場合に、メソッド呼び出し箇所を乗っ取って別のメソッドに差し替えてしまう仕組みを検討中で、 それがインターセプター(interceptor: 妨害者、途中で奪う・捕まえるもの)です。 C# 12 時点でプレビュー機能として実装されていて、後述するオプションを設定すると現在でも動かすことができます (提案ドキュメント: Interceptors)。

現在の実装をベースに話すと、 先ほどのコードが F:/src/ConsoleApp1/ConsoleApp1/Program.cs というパスのファイルに書かれているものとして、 以下のようなコードを作ります(Source Generator で作ること前提)。

using System.Runtime.CompilerServices;

namespace ConsoleApp1
{
    static class Interceptors
    {
        [InterceptsLocation("F:/src/ConsoleApp1/ConsoleApp1/Program.cs", 4, 15)]
        internal static int Parse123(string _) => 123;

        [InterceptsLocation("F:/src/ConsoleApp1/ConsoleApp1/Program.cs", 5, 15)]
        internal static int Parse456(string _) => 456;
    }
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : Attribute
    {
        public InterceptsLocationAttribute(string filePath, int line, int column) { }
    }
}

InterceptsLocation 属性が付いたメソッドで、属性で指定したファイル・行・列にあるメソッド呼び出しを乗っ取ります。

この例の場合、

  • ("略/Program.cs", 4, 15)int.Parse("123") の場所 → ここを Parse123 で乗っ取る
  • ("略/Program.cs", 5, 15)int.Parse("456") の場所 → ここを Parse456 で乗っ取る

という挙動。 その結果、Program.cs の内容は以下のものに置き換わったものとしてコンパイルされます。

using static System.Console;

// const string の Parse はコンパイル時にできるのでは。
WriteLine(Interceptors.Parse123("123"));
WriteLine(Interceptors.Parse456("456"));

// こういうのは無理として。
WriteLine(int.Parse(ReadLine()!));

ちなみに、ファイル指定がフルパスなのが気持ち悪すぎていまいち取り上げる気になれず、今までブログ化していなかったり。 後述しますが、フルパスだと困るであろうことは課題として認識されていて、C# 13 正式リリースまでには対処が入ると思います。

プレビュー オプション

インターセプターは「早めに動くものを提供して、ASP.​NET チーム辺りで実際に使ってもらってフィードバックをもらいたい」という意図で、かなり早い段階でプレビュー機能としてリリースされています。 当然作ってる側も自信をもってリリースしているわけがなく、 大幅に変更がかかる可能性があります。

(ちなみに、実際に試してもらってるコードはここ: Http.Extensions)

そんな感じの早期プレビューなのと、 先ほどの例の int.Parse みたいな何の変哲もない普通のメソッドを乗っ取れる仕組みが結構怖いので、 オプション指定しないとコンパイルできません。

当初は csproj に <Features>InterceptorsPreview<Features> というタグを入れておけば使える仕様でした。 単に「LangVersion preview」ではなくて、フィーチャースイッチを明示。

が、その後、「もっと制限をかけたい」ということになったみたいで、 <InterceptorsPreviewNamespaces>ConsoleApp1</InterceptorsPreviewNamespaces> みたいに、 「特定の名前空間にあるインターセプターのみを認める」というオプションの書き方に変わりました。 (コンパイラーがインターセプターを検索するコストを減らすためだそうです。 今は InterceptorsPreviewNamespaces という名前なものの、 プレビューが外れたら InterceptorsNamespaces にする予定。)

現在の open issue

ということで、最近あった検討内容:

相対パス化

一番はやっぱり「フルパス問題」。

Source Generator はそれなりに重たい処理になることがあって、 コンパイルのたびに何度も走ってほしくないということがあります。 そういう場合ように、Source Generator の実行結果をファイル システムに書きこんじゃって、 手書きのコードと一緒にコミットしてしまうという運用も考えられます (EmitCompilerGeneratedFiles オプションでできます)。

フルパスはこのオプションを使うと一発で破綻します。 自分の手元では F:/src/ConsoleApp1 かもしれないものの、 Git とかで他人と共有すると、他の人のパスは F:/users/UserName/repos/ConsoleApp1 とかだったりします。 都度コード生成するなら問題ありませんが、コミットしようとするとパスの差で困ります。

なので、InterceptsLocation 属性に渡すパスは相対パスにできるようになると思います。 とはいえ、以下のような課題あり。

  • 「インターセプトする側とされる側の保存先が別ドライブにある」みたいな場合、相対パス指定できない
    • 元からあるファイルシステムの問題で、#line ディレクティブとかでも同様の制限あり
  • Source Generator が生成するファイルのパスがわからないと「そこからインターセプトされる側」の相対パスがわからない
    • Source Generator の生成結果の出力先パスを相対パス フレンドリーな場所に変更するつもりあり

location specifier

もしくは、ファイル パスに依存すること自体をやめて、何らかの抽象的な「location specifier (場所指定子)」を受け付ける仕組みを用意するという案も出ています。 まずは InterceptsLocation 属性に以下のコンストラクター追加。

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
sealed class InterceptsLocationAttribute : Attribute
{
    public InterceptsLocationAttribute(string filePath, int line, int column) { }
    public InterceptsLocationAttribute(string locationSpecifier) { }
}

一例として以下のように、何らかの書式で「ソースコードの場所」がわかる文字列を指定。

class Interceptors
{
    [InterceptsLocation("v1:../../src/MyFile.cs(12,34)")]
    public void Interceptor() { }
}

独特な書式を覚えるのは大変でしょうが、 幸い、インターセプターは Source Generator 用の機能なわけで、 Source Generator 作者向けに location specifier を取得できる API を同時に提供するつもりだそうです (なので独特な書式を覚える必要はないはず)。

namespace Microsoft.CodeAnalysis;

public readonly struct SourceProductionContext
{
    public void AddSource(string hintName, string source);
    public string GetInterceptsLocationSpecifier(InvocationExpressionSyntax intercepted, string interceptorFileHintName);
}

Roslyn の構文木ノードを受け取って、 そこに相当する specifier を返してもらって、 この specifier をそのまま InterceptsLocation 属性に出力する想定です。

もっと先の話

「たぶん C# 13 より後」と分類されている課題もいくつか。

  • プロパティとかコンストラクターもインターセプトしたい
  • インターセプトする側とされる側で完全にシグネチャが同じでないとダメなのを緩和したい
    • DelegateFunc<T> に変える」みたいなことはできていいはず

ファーストクラスな Span 型

$
0
0

今日は「Span<T>ReadOnlySpan<T> をコンパイラーで特別扱いしたい」という話。

C# 7.2 の頃、Span<T>が追加されて、 安全性を損なわずに unsafe コード並みにパフォーマンスのよいコードが書けるようになりました。 それ以来、.NET の標準ライブラリでもいろんな場面でSpan<T> 型が活用されています。

いまや結構重要なポジションを担う型なわけですが、 現状の扱いはあくまで「普通の構造体の1つ」です。 そのため微妙にオーバーロード解決とかで困り気味。

例えば直近では、C# 12 でコレクション式を導入するにあたって「普通にやってたら使い勝手が悪いので Span を特別扱い」みたいなことをやっています。

// 普通にやると IEnumerable と Span の優先度はつかなくてコンパイルエラー。
EnumerableVsSpan.M(new int[5]);

// コレクション式は Span を優先する。
EnumerableVsSpan.M([1, 2, 3]);

// Span を優先しちゃう(パフォーマンス的に好ましくない)。
SpanVsReadOnlySpan.M(new int[5]);

// ReadOnlySpan を優先するよう特別扱い。
SpanVsReadOnlySpan.M([1, 2, 3]);

class EnumerableVsSpan
{
    public static void M(IEnumerable<int> _) { }
    public static void M(Span<int> _) { }
}

class SpanVsReadOnlySpan
{
    public static void M(ReadOnlySpan<int> _) { }
    public static void M(Span<int> _) { }
}

また、いわゆる共変性の辺りが微妙だったりします。 以下のように、コンパイルでこてほしくないのに実行時エラーになるのが1件、 コンパイルできてほしいのにできないのが1件。

var strArray = new string[5];

// 行ける。 Span に implicit operator が定義されているので。
Span<string> strSpan = strArray;

// なぜか行ける…
// 配列に共変性(object[] objArray = strArray; が合法という負の遺産)があるせい。
// が、実行時例外起こす。
Span<object> objSpan = strArray;

// 行ける。
ReadOnlySpan<string> strRos1 = strArray;
ReadOnlySpan<string> strRos2 = strSpan;

// これも行ける。
ReadOnlySpan<object> objRos1 = objSpan;
ReadOnlySpan<object> objRos2 = strArray;

// ダメ…
// (できても問題ないけど、ReadOnlySpan を特別扱いしないとコンパイラーにはそれがわからない。)
ReadOnlySpan<object> objRos3 = strSpan;

ということで、まあ、 「Span<T>ReadOnlySpan<T> をコンパイラーで特別扱いしたい」という話になります。

「配列から IEnumerable<T> への変換」とかが元からそうなんで、その辺りに並べて Span<T>ReadOnlySpan<T> がらみの仕様を入れるとのこと。

提案は2月5日の Language Design Meeting であっさり了承されてるし、 割かしコレクション式の取り組みからの流れっぽい感じもするので C# 13 に入りそうな感じがしますね。 懸念として、ちょっとした(めったに起こらなさそうな)破壊的変更があり得るので、 そのリスクがどう評価されるか次第。

ジェネリック型引数の部分型推論

$
0
0

C# のジェネリック型引数の推論を賢くしたいという話は、issue として記録されている分に限っても5年くらい前からあります。

現状の C# の型推論は割と "All or Nothing" で、 new() みたいに型全体の省略はできても、new List<>() みたいな「一部分だけ省略」ができません。

// 型全体の推論は可能。
// 左辺から型が決定されて、new() は new List<int>() と解釈される。
List<int> x1 = new();

// 一方、型引数だけの省略というのができない。
List<int> x2 = new List<>(); // 要は Java のダイヤモンド演算子みたいなのとか、
List<int> x3 = new List<_>(); // あるいは「ここは推論して」を表すキーワードを用意したい。

// 特にこういう「部分推論」ができないと困るのは以下のような場面。
IList<int> x4 = new List<_>(); // 左辺がインターフェイス、右辺が具象型なので new() とは書けない。

この issue はずいぶん前から「Champion」(C# チーム内の誰かが興味を持って推進している状態)にはなっていたんですが、しばらく動きはありませんでした。

それが去年くらいから動きあり。 具体的な提案が出ました。

現状有力な案は「推論したいところに _ を入れる」文法です。 先日の「C# での破壊的変更の今後の扱い」の話とも関連していますね。 discard と同じく _ を利用するとなると、class _ { } が邪魔なので。

そして最近これが Language Design Meeting でも取り上げられたみたいです。

これによれば以下のような感じ。

  • コミュニティ貢献。作者は修士論文として取り組んでいるらしい
  • C# チームも乗り気でサポート中
    • 特に、「型推論が面倒だからインターフェイス使わない」(IEnumerable<T> じゃなくて List<T> を使う方を好む)みたいな問題を減らせる点を評価
  • 出ている提案はパフォーマンスも考慮していて、「コンパイル時間が指数的に爆発する」みたいな問題は避けれそう
    • いくつか制限することで、(型推論アルゴリズムとして有名な) Hindley-Milner ほど複雑にはならず、指数的な処理は避けれる
  • ポリシーとして「var は使わない」とか「型が明白な場合にのみ var を使う」みたいなことがあり、現在の var と同じく部分型推論にもその手のアナライザー提供の可能性あり
    • ただ、部分型推論にとって「型が明白」とはどういうものかは要検討
  • C# 13 には入らなさそう(原文 "not going to" なので結構な確度で入らない)なものの、取り組みには前向き

オーバーロード解決優先度

$
0
0

今日は「負の遺産整理で消したいけども消せないメソッド対処」の話。 紆余曲折合って、現状、OverloadResolutionPriority 属性でオーバーロード解決に優先度をつけて、 優先度の高いものだけを見るようにするという案になっています。

最近のわかりやすい例だと、「パフォーマンス改善のために配列引数を ReadOnlySpan 引数に変えたい」というのをやりたいとします。

元々、配列引数で作っていたとして、

int[] x = [1, 2, 3];

C.M(x);

// 元コード。
public class C
{
    // これの引数を変えたい。
    public static void M(int[] x) { }
}

暗黙的型変換があるものであれば、多少型を変えても「再コンパイルすれば大丈夫」という状態になることはあります。

int[] x = [1, 2, 3];

// int[] → ReadOnlySpan<int> の変更は、再コンパイルするならエラーにならず移行可能。
C.M(x);

// 変更後コード。
public class C
{
    // 引数、変えちゃった。
    public static void M(ReadOnlySpan<int> x) { }
}

ただ、「再コンパイル必須」というのは、 末端のアプリならともかく、ライブラリとかプラグインとかにとってはきついです。 過去にコンパイル済みバイナリの形でライブラリ参照すると、 先ほどの例は「M(int[]) が見つからない」という実行時例外を起こします。

なので、現実的には「メソッドは追加する一方」になりがちなんですが、 非推奨にしたい古いメソッドによって利便性が損なわれることが多々あります。

int[] x = [1, 2, 3];

// 普通に書くと int[] の方に行っちゃう。
// パフォーマンスを理由に ReadOnlySpan<int> オーバーロードを足したのに無意味。
C.M(x);

// 変更後コード。
public class C
{
    // 元のメソッドは残しつつ、
    public static void M(int[] x) { }
    // オーバーロードを追加。
    public static void M(ReadOnlySpan<int> x) { }
}

非推奨にしたいものには Obsolete 属性を付けるという手段はありますが、 Obsolete 属性を付けたところでオーバーロード解決候補には残ってしまうのがかなり邪魔です。

int[] x = [1, 2, 3];

// C# コンパイラーは Obsolete 属性が付いたメソッドも普通にオーバーロード解決候補にしちゃう。
// ReadOnlySpan<int> の方を呼んでほしくてやってるのに、
// 実際は int[] が選ばれたうえで警告が出るだけになる。
C.M(x);

// 変更後コード。
public class C
{
    // 古い方には Obsolete 属性を付ける。
    [Obsolete("Use M(ReadOnlySpan<int> x) instead.")]
    public static void M(int[] x) { }

    public static void M(ReadOnlySpan<int> x) { }
}

ということで、「バイナリ互換性のために残すけども、コンパイル時のオーバーロード解決候補には残さない」 (過去にコンパイルした DLL からは見えてるけども、ソースコードの再コンパイル時には見えない) という状態を作りたいという要望があります。 これを「binary compat only」とか呼んでいます。

最初に思いつく案としては、Obsolete 属性に手を入れる方法。

public class C
{
    // 最初に思いつく案として、Obsolete 属性を修正。
    [Obsolete("Use M(ReadOnlySpan<int> x) instead.", ObsoleteLevel.BinaryCompatOnly)]
    public static void M(int[] x) { }

    public static void M(ReadOnlySpan<int> x) { }
}

ただ、既存の Obsolete 属性に手を入れる案だと、例えば netstarndard2.0 向けライブラリとか、 ターゲットフレームワーク古いライブラリに対して使えなくなります。 なので新しい属性を足さざるを得ず。

当初案はまんま BinaryCompatOnly 属性でした。

int[] x = [1, 2, 3];

// ReadOnlySpan<int> の方が選ばれるようになる予定。
C.M(x);

public class C
{
    // 新属性で「オーバーロード解決候補から外す」指定。
    [BinaryCompatOnly]
    public static void M(int[] x) { }

    public static void M(ReadOnlySpan<int> x) { }
}

public class BinaryCompatOnlyAttribute : Attribute;

ところが、じゃあ、「完全に候補から外す」だけでいいのかというと、そうでもなくて困ったみたいです。 例えば、インターフェイスの実装とかはどうするの?ということになりました。

public interface I
{
    // 新属性で「オーバーロード解決候補から外す」指定。
    [BinaryCompatOnly]
    void M(int[] x);

    // 新規追加メソッド。
    void M(ReadOnlySpan<int> x);
}

public class C : I
{
    // BinaryCompatOnly = コンパイル時には見えない
    // なわけで、 I.M(int[]) も「見えない」 = 実装できないのが正しい?
    //
    // こっちの M(int[]) にも BinaryCompatOnly 属性を付けることを義務付ける?
    public void M(int[] x) { }

    public void M(ReadOnlySpan<int> x) { }
}

public class BinaryCompatOnlyAttribute : Attribute;

そこで最終的に、

  • オーバーロード解決に優先度をつけれるようにする
    • 何も指定がないときを 0 として、数字が大きいほど優先度を上げ、小さいほど下げる
  • 優先度が一番高いものだけを候補にする

という案に修正されました。 属性名は OverloadResolutionPriority

int[] x = [1, 2, 3];

// ReadOnlySpan<int> の方が選ばれるようになる予定。
C.M(x);

public class C
{
    // 優先度を上げたければ priority の数字を増やす。
    [OverloadResolutionPriority(1)]
    public static void M(int[] x) { }

    // 逆にこっちに priority = -1 とかを与えて優先度を下げるとかでも OK。
    public static void M(ReadOnlySpan<int> x) { }
}

public class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
    public int Priority { get; } = priority;
}

これなら先ほどのインターフェイスの例みたいな「見なくなりすぎる」問題は回避。 「高優先度のものが見つからなければ単に古い方を見に行く」みたいな挙動になります

まあ、具体化するには検討すべき項目はまだまだあるでしょうが (例えば優先度は int で何でも受け付けるのでいいか?とか)、 方向性としては、C# チームも強く支持するし、 BCL 側もこれが入れば大々的に使いたい意向ありとのこと。

field, value を文脈キーワード化

$
0
0

C# 13 向けに検討されている機能の一つに、 「半自動プロパティ」とか「field キーワード」と呼ばれているものがあります。 元々は C# 12 向けに考えられていて、去年、うちのブログでも書いているやつです。

簡単におさらいすると、 プロパティの get/set アクセサー内で、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 候補(改め、13 候補)】 半自動プロパティ (semi-auto-property)。
    // バッキング フィールドは自動生成。
    // 全自動の方と違って、バッキング フィールドの使い方は自由にできる。
    // field キーワードでバッキング フィールドを読み書き。
    public int Z { get => field; set => field = value; }
}

C# 12 時点では「これを破壊的変更なしで実装するのは大変」ということで見送りになりまして、 その結果検討されていたのが先日書いたブログの話。

ここで、「field の扱いで破壊的変更があるんだったら、value についても…」 という話が出ています。

というのも、value (プロパティの set 内でだけ特別な意味を持つ)はちょっと C# 的には珍しく、 キーワードではなくて「暗黙に定義された引数」で、ちょっと浮いた挙動をします。

1つ目、@ で「脱キーワード化」ができない。

class A
{
    public int X
    {
        set
        {
            // value は @ を付けてもダメ。
            // 扱いが「暗黙定義された引数」なので、@value もその引数を指す。
            var @value = 1;

            // 普通、キーワードだったら @ を付けることで識別子に使える。
            var @this = 2;
        }
    }
}

2つ目、nameof

class A
{
    public int X
    {
        set
        {
            // 逆に、引数扱いゆえに nameof が使える。
            var n1 = nameof(value);

            // キーワードには nameof は使えない。
            var n2 = nameof(this);
        }
    }
}

3つ目、外側の識別子の参照。

class A
{
    int value;
    int @this;

    public int X
    {
        set
        {
            // 外にある「value フィールド」すら、@value では参照できない。
            // 暗黙の引数の方になる(@ を付けるだけ無駄)。
            // (ちなみに、 this.value = 1; と書けばフィールド参照になる。)
            @value = 1;

            // キーワードの場合は @this で外のフィールド参照になる。
            @this = 2;
        }
    }
}

field を足すことで軽微ながら破壊的変更が出るんなら、 value に軽微な破壊的変更がかかってもいいのではということで、 もうこの際 value もキーワード(set 内限定なので、文脈キーワード)してもいいのではという話になります。 どういう影響があるかというと、先ほどの例からわかる通りで、

  • var @value = 1; みたいなのが書けるようになる
    • これは、できないことができるようにあるので破壊的ではない
  • nameof(value) が書けなくなる
    • こう書いていた人が多数派とは思えない
    • ※追記: if (value is null) throw new ArgumentNullException(nameof(value)); って書く人それなりにいる説あり
  • @value = 1; みたいなのが暗黙的引数の上書きから、外のフィールドの上書きに変わる
    • 単に value = 1; でよかったわけで、もともと変

となります。

ちなみに、field は「暗黙の引数扱い」でも「文脈キーワード扱い」でもどちらにしろ破壊的変更になります。 「文脈キーワード扱い」の方が自然っぽいんですが、 そうなるとこの「value と何か挙動が違う」が気になるという懸念がありまして。 そこで出た対案が「value も文脈キーワードに変更」という感じかと思います。

ラムダ式の引数で、型名を省略して ref, out などだけを指定

$
0
0

ラムダ式で、ref 引数などに対して ref x => { } みたいに書けるようにしたいという話が出ています。

ラムダ式での ref 引数、out 引数

ラムダ式は、状況が許すなら、x => { } などといったように非常に簡素に書けます。 ところが、refout が絡むとそうもいかなくて、型推論が効く状況でも型名を省略できません。

// 通常、ラムダ式は型推論が効く限り、引数の型を省略できる。
Action<int> a = x => { };

// ところが ref, out などの修飾が付いた引数は省略不可。

// これなら OK。
RefAction<int> r = (ref int x) => { };
OutFunc<int> o = (out int x) => x = 1;

// ダメ。CS1676 エラー。
RefAction<int> r1 = x => { };
OutFunc<int> o1 = x => x = 1;

delegate void RefAction<T>(ref T arg);
delegate void OutFunc<T>(out T arg);

特に、「他にも引数が多かったり、他の引数のどれかに型名が長くて書きたくない引数がある」みたいな状況では相当に不便です。

// 全部の引数に型の明示が必要。
ManyParams a = (int x, int y, int z, ref int r) => { };

// r の型は省略できない。
ManyParams a1 = (x, y, z, r) => { };

// 「部分的に型を明示」というのも書けない。
ManyParams a2 = (x, y, z, ref int r) => { };

delegate void ManyParams(int x, int y, int z, ref int r);
// 全部の引数に型の明示が必要。
LongTypeName a = (IReadOnlyDictionary<(int x, int y), List<string[,]>> x, ref int r) => { };

// r の型は省略できない。
LongTypeName a1 = (x, r) => { };

// 「部分的に型を明示」というのも書けない。
LongTypeName a2 = (x, ref int r) => { };

delegate void LongTypeName(IReadOnlyDictionary<(int x, int y), List<string[,]>> x, ref int r);

これに対して、ref x => { } みたいな書き方は認めてもいいんじゃない?という話があります。

// 現状ダメ。でも、これくらいはできてもいいのでは?
RefAction<int> r = (ref x) => { };
OutFunc<int> o = (out x) => x = 1;
ManyParams m = (x, y, z, ref r) => { };
LongTypeName l = (x, ref r) => { };

delegate void RefAction<T>(ref T arg);
delegate void OutFunc<T>(out T arg);
delegate void ManyParams(int x, int y, int z, ref int r);
delegate void LongTypeName(IReadOnlyDictionary<(int x, int y), List<string[,]>> x, ref int r);

コミュニティ提案

実際アイディア自体は2015年くらいからずっとあります。

ref 引数ラムダ式とか自体が使用頻度低めなのでそれほど優先度はついておらず、 ずっと「Any Time」(C# チーム自らはやらず、「コミュニティ貢献お待ちしております」状態)でした。

これに対して、去年くらいに実際、コミュニティからの提案ドキュメントが上がっていました。

履歴)を見るに、2023年7~8月くらいにコミュニティから提案されていて、C# 12 作業中は進捗なし。 今月に入ってから C# チームの中の人が引き取って検討を始めていそうな感じですね。

そして数日前の Language Design Meeting で議題に。 とりあえず提案は承認されたみたいです。

その他検討事項

Desing Meeting では対案も2点ほど検討されたんですがそちらはリジェクト。 元の提案の方向で受け付けるみたいです。

対案その1は、x => { } だけで ref/out も「推論」してもいいのでは?という案。 ただ、C# の ref 引数、out 引数は、呼び出し元にも ref/out の明示を求めるくらいなので、さすがに x => { } というような書き方はちょっと C# 的には違和感があります(なのでリジェクト)。

RefAction<int> r = (ref int x) => { };
OutFunc<int> o = (out int x) => x = 1;

// ref, out 引数は呼び出し側にも ref, out を書く必要があるくらい明示を求められる。
// 呼び出し先で書き変わるのは明示されないと怖い。
int local;
o(out local);
r(ref local);

// なのでラムダ式側でも ref, out は書かないと違和感。
// 以下のような書き方は今後も乗り気ではない。
RefAction<int> r1 = x => { };
OutFunc<int> o1 = x => x = 1;

delegate void RefAction<T>(ref T arg);
delegate void OutFunc<T>(out T arg);

対案その2は前述の ManyParams とか LongTypeName とかの例で書いたような、「引数の一部分の型名を省略、一部分を明示」です。 ただ、これは「r に指定した型から x の型を推論」みたいな別の要望が加わるだろうことと、 それをやると部分型推論の話と同様、 推論を頑張ろうとすると指数的なコンパイル時間になってしまう可能性があってちょっと怖いそうです(なのでリジェクト、やるとしても部分型推論と一緒に)。

// ラムダ式引数の部分型指定 + 型引数の推論。
// 結構推論機構が複雑になるはず。
static ManyParams<T> Create<T>(ManyParams<T> a) => a;
var a1 = Create((x, y, z, ref int r) => { });

delegate void ManyParams<T>(T x, T y, T z, ref T r);

あと、元の提案に残っていた「属性や、引数のデフォルト値はどうしよう?」という未解決の議題についても「大変そうなわりに需要がない」ということで、やらないことになりそうです。

using System.Diagnostics.CodeAnalysis;

// C# 10 と 12 で、こんな感じで属性を付けたりデフォルト値を指定できるようになった。
Func<string, int> f = ([MaybeNull] string s = null) => s?.Length ?? 0;

// これに対して型名省略したい?
// (そんなに需要なさそうな割に、これを実装するのは大変そう。)
Func<string, int> f1 = ([MaybeNull] s = null) => s?.Length ?? 0;

params コレクション

$
0
0

ほぼ1年ぶりparams の話。

params を配列以外のコレクションに対して使えるようにするという話ですが、 雰囲気的に C# 13 でついに 入りそうです。 なので、最近そこそこ高頻度で Language Design Meeting の議題に上がっています。

まあ、割かしもう詳細を詰めている感じの話題が多めですね。

params ‘コレクション’

去年には「ReadOnlySpan<T> 以外需要低め」、「他はコレクション式を使って M([a, b, c]) でいいのでは」などという話も出ていましたが。 コレクション式を実装した今改めて検討して、 むしろ「コレクション式とそろえるのがいいのではないか」という感じに変わったみたいです。

// ReadOnlySpan を優先するようになる予定。
C.M(1, 2, 3);

class C
{
    // 今でも書ける。
    public static void M(params int[] _) { }

    // 新規に書けるようになる予定。
    public static void M(params List<int> _) { }
    public static void M(params ReadOnlySpan<int> _) { }
}

params ‘ref struct’

params に配列以外の型を認めたいという話の前提には、パフォーマンスを改善したいという要求があります。 なので、SpanReadOnlySpan をはじめとした ref struct を使いたいです(ref struct 自体がパフォーマンス改善のために導入された概念)。

で、ref struct にはスコープの概念があって、引数や変数を scoped で修飾するかどうかでちょっと挙動が変わります。

M(true);

static S M(bool b)
{
    if(b)
    {
        // [] が作る Span が S に伝搬してて、外に漏らせないので return に渡すとエラー。
        return C.Unscoped([1, 2, 3]);
    }
    else
    {
        // こちらは Span が伝搬しないので return できる。
        return C.Scoped([1, 2, 3]);
    }
}

class C
{
    // span の寿命が S に伝搬する。
    public static S Unscoped(Span<int> span) => new(span);

    // span の寿命を外に漏らさない。
    // なので、S に直接伝搬できない。
    // new(span.ToArray()) とかする必要がある。
    public static S Scoped(scoped Span<int> span) => new(span);
}

readonly ref struct S(Span<int> span)
{
    public readonly Span<int> Span = span;
}

で、ここに params をつけれるようになった場合にどうするかという話になります。

まあ、現状出ている用途を考えると「scoped じゃない params を必要とする場面はなく、scoped な params を必要とする場面はある」とのことで、「params が付いている時点で暗黙的に scoped にする」という判断になりそうです。

こうなるともう1つ問題が、オーバーライドをどうするかという話があるみたいです。 というのも、params 配列の場合、実はオーバーライド側には params 修飾を付けなくてもいいそうで。

class Base
{
    public virtual void M(params int[] x) { }
}

class Derived : Base
{
    // params 配列の場合、派生側で params を付けなくても別にいい。
    public override void M(int[] x) { }
}

ところがまあ、「params ref strcut は暗黙的に scoped」みたいな暗黙の挙動があるので、 「何もつけてないのになぜか scoped」みたいな挙動は避けたいでしょう。 なので、この場合は「オーバーライド側にも params を必須にしたい」とのこと。

(けど、LDM に挙げられている例を見るに、戻り値があるときにだけこれを求められていそう…)

オーバーロード解決

現在の params (配列の params T[])とコレクション式は、ちょっとオーバーロード解決の仕組みが違います。 なので、「params の部分を [] で覆っても同じ結果になる」というのは成り立たないことになります。 例えば以下のようなもの。

C.M([1, 2, 3]); // こちらは解決できなくてエラーに。
C.M(1, 2, 3); // こちらは int[] 側に解決。

class C
{
    public static void M(params int[] _) { }
    public static void M(params long[] _) { }
}

で、params コレクションに関してですが、「既存の params 配列に沿う」案で行くみたいです。

引数の評価順

引数に副作用のある式を渡さない限り問題になることは少ないので忘れがちですが、 引数をどういう順で評価するかは決めておかないと混乱のもとです。 C# は基本的に「呼び出し側で並べた順」で、例えば名前付き引数を使うと順序を変えることができたりします。

Test(GetA(), GetB()); // A → B
Test(b: GetB(), a: GetA()); // B → A

static void Test(int a, int b) { }

static int GetA() { Console.WriteLine("A"); return 0; }
static int GetB() { Console.WriteLine("B"); return 0; }

で、名前付き引数を使うと params 引数の場所も末尾以外に移せたり。

Test(b: GetB(), c: GetC(), a: GetA()); // B → C → A

static void Test(int a, int b, params int[] c) { }

static int GetA() { Console.WriteLine("A"); return 0; }
static int GetB() { Console.WriteLine("B"); return 0; }
static int GetC() { Console.WriteLine("C"); return 0; }

ちなみにこの時、params int[] c のための配列は、Test を呼ぶ直前になるそうです。 ということで、展開結果は以下のような感じ。

var b = GetB();
var c = GetC();
var a = GetA();
var paramsC = new[] { c };
Test(a, b, paramsC);

ところが、params コレクションとなるとどうなるべきかという話になります。 コレクションのインスタンスはいつ作られるべきなのか。

Test(b: GetB(), c: GetC(), a: GetA());

static void Test(int a, int b, params MyCollection c) { }

static int GetA() { Console.WriteLine("A"); return 0; }
static int GetB() { Console.WriteLine("B"); return 0; }
static int GetC() { Console.WriteLine("C"); return 0; }

class MyCollection : IEnumerable<int>
{
    public MyCollection() { Console.WriteLine("MyCollection Construcotr"); }
    public void Add(int _) { }
    public IEnumerator<int> GetEnumerator() => throw new NotImplementedException();
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

こんな副作用の起こし方をするコードはめったに書かないでしょうけども、 "MyCollection Construcotr" はいつ表示されるべきでしょう?

とりあえず現状は、B → MyCollection → C → A の順で考えているそうです。 引数 c: の場所で生成。GetC を呼ぶよりも前。 要するに、以下のように展開したいんでしょうね。

var b = GetB();
var a = GetA();
var paramsC = new MyCollection();
paramsC.Add(GetC());
Test(a, b, paramsC);

メタデータ

今ある params 配列のコンパイル結果には System.ParamArrayAttribute が付きます。 で、C# 13 で考えている params コレクションでも、別にこの属性を使いまわすこともできるそうです。

ただ1点懸念は、C# 以外のコンパイラーが誤動作しないかどうか。 新しい属性であれば「未対応なので無視」でいいわけですが、 既存の属性を使いまわすと「ParamArray 属性が付いているのであれば配列でないとダメ」というコンパイル エラーを起こす可能性が高いです。

ということで、新しい params コレクションについては新しい属性として System.Runtime.CompilerServices.ParamCollectionAttribute を用意するそうです。


IList とかを IReadOnlyList とかから派生させたい

$
0
0

.NET が長らく抱えている「なぜ IList<T>IReadOnlyList<T> ではないのか」問題、 .NET 9 で解消するかもしれないみたい。

ちなみに、問題を抱えるに至った原因は IReadOnlyList<T> が後付けということです。 1から作り直すのであれば、誰がどう考えても IList<T>IReadOnlyList<T> から派生させるのが自然です。 それがかえって、IReadOnlyList<T> 導入以降に .NET 利用を始めた人に混乱を招いているというのが現状になります。

当初設計: インターフェイスは増やしすぎない

インターフェイスを増やすというのは、 型情報で DLL サイズが増えるとか、 実行時にインターフェイスを検索するコストが増えるとか、 多少なりともコストを生じます。

一方で、.NET Framework の最初のβ版が出たのは2000年ごろ、正式版で2002年なわけですが、 この頃は read-only であることの重要性が過小評価されていたと思います。 なので、重要でない(と当時は思われていた)ものにコストは掛けたくないという話に。

(この辺りのことは「.NETのクラスライブラリ設計」で触れられていたりします。 ちなみにこの本、要は「.NET の初期設計に関する懺悔本」です。)

そこで当時の設計としては「read-only / writeable なインターフェイスを1個用意して、IsReadOnly プロパティで書き込み出来るかどうかを調べる」という作りでした。

namespace System.Collections;

public interface IList : ICollection, IEnumerable
{
    object this[int index] { get; set; }
    bool IsReadOnly { get; } // ← これ
    void Add(object value);
    // 以下略
}

.NET Framework 2.0 (2005年)にジェネリクスが導入されてもまだこの思想は引き継がれます。 まあ、旧来インターフェイスとジェネリック インターフェイスで思想が違うのも混乱しそうですし。

namespace System.Collections.Generic;

public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
    int Count { get; }
    bool IsReadOnly { get; } // ← これ
    void Add(T value);
    // 以下略
}

問題になり始めたのは C# 4.0 (2010年)で共変性を得てからでして。 読み書き両方できてしまう IList<T>ICollection<T> では、以下のような共変な代入ができません。

IList<string> str = new List<string>();
IList<object> obj = str; // ダメ。

// そりゃ、こういうコード書かれたらまずいので当然。
obj.Add(1);

そこで .NET Framework 4.5 (2012年)では read-only 系のインターフェイスが導入されます。

IReadOnlyList<string> str = new List<string> { "abc" };
IReadOnlyList<object> obj = str; // read-only なら共変。

// obj.Add(1); とか書かれる心配がない。
// 読むだけなら安全。
Console.WriteLine(obj[0]);

インターフェイスへの親インターフェイスの追加・メンバー移動は破壊的変更

2012年に追加された read-only 系インターフェイスですが、元々あったインターフェイスとは独立しています。 残念ながら「IList<T>IReadOnlyList<T> ではない」という状態。

namespace System.Collections.Generic;

public interface IReadOnlyCollection<out T> : IEnumerable<T>
{
    int Count { get; }
}

public interface ICollection<T> : IEnumerable<T>
{
    // IReadOnlyCollection とは独立に Count を持つ。
    int Count { get; }
    // 以下略
}

public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
{
    T this[int index] { get; }
}

public interface IList<T> : ICollection<T>
{
    // IReadOnlyList とは独立に this[int] を持つ。
    T this[int index] { get; set; }
    // 以下略
}

普通に考えて、1から作るのであれば以下のようにします。

namespace System.Collections.Generic;

public interface IReadOnlyCollection<out T> : IEnumerable<T>
{
    int Count { get; }
}

public interface ICollection<T> : IReadOnlyCollection<T> 
{
    // 以下略
}

ところが、後付けでこういうことをするのは破壊的変更になります。

例えば以下のようなコードがあったとします。

// バージョン1

// corelib.dll
interface ICollection
{
    int Count { get; }
}

// corelib とは別のプロジェクトで、別の開発者が保守
// mylib.dll
class C : ICollection
{
    public int Count => 0;
}

ここに IReadOnlyCollection を「理想的な状態」で導入したくて Count を移動させると mylib を壊します。

// バージョン2

// corelib.dll
interface IReadOnlyCollection
{
    int Count { get; }
}

interface ICollection : IReadOnlyCollection
{
    // Count は IReadOnlyCollection に移した。
}

// corelib とは別のプロジェクトで、別の開発者が保守
// mylib.dll
class C : ICollection
{
    // 再コンパイルするなら平気。
    // ただ、古い dll のまま使うと「IReadOnlyCollection.Count がない」と怒られる。
    // 再コンパイルするまでは C が持ってるのは ICollection.Count。
    public int Count => 0;
}

ということでインターフェイスを独立。 これなら「再コンパイルするまでは CIReadOnlyCollection にはならない」というだけなので、 DLL のロードに失敗したりはしません。

// corelib.dll
interface IReadOnlyCollection
{
    int Count { get; }
}

interface ICollection
{
    int Count { get; } // IReadOnlyCollection と機能がダブってるけど許して
}

// corelib とは別のプロジェクトで、別の開発者が保守
// mylib.dll
class C : ICollection, IReadOnlyCollection // 2個とも実装
{
    public int Count => 0;
}

これが .NET のコレクション系インターフェイスの現状になります。

インターフェイス メソッドのデフォルト実装

.NET Core 3.0 (2019年)にデフォルト実装というものが導入されて、インターフェイスへのメンバー追加での破壊的変更を避けれるようになりました。

この機能を使えば先ほどの「既存クラスが IReadOnlyCollection.Count を実装していない」問題は解消できます。 (親インターフェイスの追加は、「メンバー追加」の一種なのでデフォルト実装で対処できます。)

// corelib.dll
interface IReadOnlyCollection
{
    int Count { get; }
}

interface ICollection : IReadOnlyCollection
{
    new int Count { get; } // IReadOnlyCollection.Count とは別の Count にはなっちゃう。

    // IReadOnlyCollection のことを知らない既存クラスのために、
    // 既存クラスに代わって ICollection 内で IReadOnlyCollection.Count を実装。
    int IReadOnlyCollection.Count => Count;
}

// corelib とは別のプロジェクトで、別の開発者が保守
// mylib.dll
class C : ICollection
{
    // 再コンパイルするまではあくまで ICollection.Count。
    // それでも、ICollection 側で IReadOnlyCollection.Count を実装してくれているので平気。
    //
    // ちなみに、再コンパイルするとこの Count をもって
    // ICollection.Count と IReadOnlyCollection.Count の両方を実装。
    public int Count => 0;
}

ということで、インターフェイスのデフォルト実装の導入後、 ついに ICollection<T>IReadOnlyCollection<T> 派生に、 IList<T>IReadOnlyList<T> 派生にできるのではないかと多くの期待が寄せられています。 実際、2019年に提案あり:

ただ、厳密にはこれも破壊的変更を起こす可能性はあったりします。 というのも、デフォルト実装には「ダイアモンド継承」問題というものがあります。 以下のような感じで、「分かれ道からの合流がある継承」をやると問題を起こすことがあります。

interface IA
{
    int M();
}

interface IB : IA
{
    int IA.M() => 1; // デフォルト実装持ち
}

interface IC : IA
{
    int IA.M() => 2; // デフォルト実装持ち
}

// IA.M の実装をデフォルト実装に頼るとして、
// IB の実装と IC の実装のどちらを使えばいいか不明瞭。
class C : IB, IC
{
}

まあ、前述の ICollection に「分かれ道」はないので誰しもがこの問題を踏むわけではないんですが。 1段自作のインターフェイスとかを挟んでいると問題を踏む可能性が出てきます。 例えば以下のような感じ。

// corelib とは別のプロジェクトで、別の開発者が保守
// anotherlib.dll
interface ICustomReadonlyList : IReadOnlyCollection
{
    // 何らかのデフォルト実装持ち
    int IReadOnlyCollection.Count => 0;
}

// corelib とも anotherlib とも別のプロジェクトで、別の開発者が保守
// mylib.dll
class C : ICollection, ICustomReadonlyList
{
    // ICollection 更新前: 
    //   ICollection.Count は明示的に実装
    //   IReadOnlyCollection.Count は ICustomReadonlyList 側のデフォルト実装を使用
    //
    // ICollection 更新後: 
    //   ICollection.Count は明示してるから平気
    //   IReadOnlyCollection.Count は ICustomReadonlyList と ICollection のどちらのデフォルト実装を使えばいいかわからない
    //   (ソースコードも修正しないと再コンパイルも失敗)
    int ICollection.Count => 1;
}

この辺りの懸念もあって、しばらく塩漬けが続きます。

ついに動きが

そして時は流れること4年、ついに動きが。 .NET 9 でこの作業をやろうという検討に入ったみたいです。

C# 13 でのコレクション式 - 制限の緩和の話

$
0
0

C# 13 でのコレクション式 - 制限の緩和の話

C# 12 でコレクション式が入ったわけですが、 スケジュールの都合で「C# 12 後に改めて検討する」ということになった機能がたくさんあります。 C# 12 リリース(2023/11)直後から再検討が始まっていて、先月にはある程度まとまった計画が出ています。

量が多いのでちょっとずつ取り上げ…

  • ディクショナリ式
  • 自然な型
  • インラインなコレクション式
  • コレクションに対する拡張メソッド
  • 現状でコレクション式に対応してない型
  • 非ジェネリックなコレクションのサポート
  • 制限の緩和 ← 今日はこれ

制限の緩和

今、コレクション式の要素の型は IEnumerable<T>T で判定しています。

using System.Collections;

foreach (var x in new A()) ; // この x は int

// Add(int) だけあればよさそうに見えるのに、
// 実際には IEnumerable<int> をみて「int のコレクション」と判断してる。
A a = [1];

// foreach すると int を列挙する型。
class A : IEnumerable<int>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw new NotImplementedException();
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
    public void Add(int x) { }
}
// foreach はインターフェイスがなくても GetEnumerator っていう名前のメソッドさえ持っていれば OK なのに。
foreach (var x in new A()) { }

// これはダメになる。
A a = [1];

// インターフェイスを削るとコレクション式で使えなくなる。
class A
{
    public IEnumerator<int> GetEnumerator() => throw new NotImplementedException();
    public void Add(int x) { }
}
using System.Collections;

// foreach なんとか OK。
// non-generic な GetEnumerator が呼ばれてるので object を介してるけど…
foreach (int x in new A()) { }

// 旧来のコレクション初期化子は使えるのに…
A a1 = new() { 1 };

// コレクション式はダメになる。
A a2 = [1];

// non-generic インターフェイスに変えると?
class A : IEnumerable
{
    public IEnumerator GetEnumerator() => throw new NotImplementedException();
    public void Add(int x) { }
}

ちなみに、この「IEnumerable<T>T」以外は受け付けなかったりします。 これも、コレクション初期化子時代はできたこと。

using System.Collections;

// 旧来のコレクション初期化子は string を受け付けるのに…
A a1 = new() { 1, "2" };

// コレクション式はダメになる。
A a2 = [1, "2"];

// Add だけは string 受付。
class A : IEnumerable<int>
{
    public void Add(int x) { }
    public void Add(string x) { }

    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw new NotImplementedException();
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

これが、非ジェネリックな IEnumerable を使うと object のみ受け付けるようになるみたいです。 しかもこれ、 Visual Studio 17.10 以前であれば受け付けていたコードがコンパイル エラーになるというひと悶着あり。

using System.Collections;

// 旧来のコレクション初期化子は string を受け付けるのに…
A a1 = new() { 1, "2" };

// これ、ちょっと前まで受け付けていたらしい。
// Visual Studio 17.10 Preview 1 だとエラー。
A a2 = [1, "2"];

// non-generic なインターフェイスを実装。
class A : IEnumerable
{
    public void Add(int x) { }
    public void Add(string x) { }

    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

意図した破壊的変更 (たぶん、1/8 の LDM での決定)だそうですが、 本当にこの変更をしてよかったのかどうか。 こういう非ジェネリック IEnumerable だけ実装して、Add でちゃんとした型を指定しているクラス、 WPF とか WinForms には結構あって、それが突然コンパイルできなくなったものでちょっとした混乱が起きています。

ちなみに、この変更の理由は、こうしておかないと params コレクションを使った時のオーバーロード解決のコストが高くなるからだそうです。 制限を緩めるとして、もしかしたら「コレクション式では使えるけども params コレクションでは使えない」みたいな状況が増えるかもしれません。

一方、そもそもとして IEnumerable 実装は必要なのかという問題が。 何せ、コレクションを作る時点では GetEnumerator は要らず、CollectionBuilder 属性で指定した Create メソッドだけあれば事足ります。 例えば、型によっては「別のコレクションを作るための足掛かりにするもので、直接列挙はしない」みたいなものがあります。 (実際、Roslyn チーム自身が1件そういう問題を踏んだりしています: CSharpTestSourceSyntaxTree[] を作るために使っていて、この型自体からの列挙はしない)。

ということで、CollectionBuilder 属性指定のコレクション型の場合、 Create メソッドの引数の ReadOnlySpan<T> から要素の型を決めようという提案が出ています。

C# 13 でのコレクション式 - ディクショナリ式

$
0
0

C# 13でのコレクション式関連、量が多いのでちょっとずつ取り上げシリーズ。

今日はディクショナリ式の話を。

  • ディクショナリ式 ← 今日はこれ
  • 自然な型
  • インラインなコレクション式
  • コレクションに対する拡張メソッド
  • 現状でコレクション式に対応してない型
  • 非ジェネリックなコレクションのサポート
  • 制限の緩和

ディクショナリ式

C# 12 でコレクション式が入りましたが、Dictionary<TKey, TValue> などのディクショナリ系の型に対しては使えませんでした。

// C# 12 でも空っぽのディクショナリは作れるのに…
Dictionary<string, int> d = [];

// 要素があるものは書く手段がない(以下はいずれもエラー)。
// スケジュールの都合で意図的に「C# 13 でやる」計画。

// KeyValuePair とかタプルも受け付けず。
Dictionary<string, int> d1 = [KeyValuePair.Create("", 1)];
Dictionary<string, int> d2 = [("", 1)];

// コレクション初期化子/オブジェクト初期化子みたいな構文も受け付けず。
Dictionary<string, int> d3 = [{"", 1}];
Dictionary<string, int> d4 = [[""] = 1];

C# 12 時点でコレクション式に対する背景と同じく、 ディクショナリについても以下の需要が見込まれます。

  • 簡素に書きたい
  • いろいろな種類のディクショナリ系の型に対して共通で使える構文にしたい
  • 特に、既存のコレクション初期化子では使えない immutable な型にも対応したい

まあ、 GitHub を軽くクロールしてみて利用頻度を調べると、 リストや配列と比べたらディクショナリの利用率は10%くらいらしいです。 とはいえ、10%もそこそこ大きな数字。 C# 12 時点では後回しになりましたが、13候補としては有力です。

提案ドキュメント、関連デザインミーティング等:

まあ検討が始まったばかりなので、まだまだ結論の出ていない検討事項も多数。 とりあえず今日は3月11日のミーティング議事録をベースにした話を書こうかと思います。

構文の候補

まだ構文をどうするかも決定ではないんですが、現状の最有力候補は [key: value] みたいな書き方です。

// 「ディクショナリ式」の最有力候補の文法:
Dictionary<string, int> d = [
    "one": 1,
    "two": 2,
    ];

もちろん、「JavaScript では {} を使うけども」みたいな別案もあるんですが、 まあ、C# 12 のコレクション式に合わせて [] になると思われます。

ちなみに、最初期には「[] の外でも key: valueKeyValuePair を作れるようにするべきか?」みたいな見当もありましたが、 現状それには否定的で、 [] の中限定の構文になりそうです。

// 没案「KeyValuePair 式」。
KeyValuePair<string, int> kvp = "one": 1;

検討事項1: KeyValuePair を並べる

ディクショナリ式中では、key: value みたいな形式のみを受け付けるか、それとも、KeyValuePair であれば直接書けるようにするかという話があります。

// key: value のみ。これは問題ない。
Dictionary<string, int> d = ["one": 1];

var kvp = KeyValuePair.Create("two", 2);

// KeyValuePair をいちいち展開する必要はあるかどうか。
Dictionary<string, int> d1 = ["one": 1, kvp.Key: kvp.Value];

// こう書きたい需要はある。
Dictionary<string, int> d2 = ["one": 1, kvp];

["one": 1, kvp] と書けるようにする案には肯定的な人が多く、承認されそうです。

検討事項2: KeyValuePair のリストを Spread する

検討事項1と似たような話ですが、IEnumerable<KeyValuePair<TKey, TValue>> とかをディクショナリ式中に含められるかという話もあります。

var kvps = new[] { KeyValuePair.Create("two", 2) };

// .. で展開すると KeyValuePair になるわけで、
// KeyValuePair を認めるのなら、 ..(KeyValuePair のリスト) も認めたい。
Dictionary<string, int> d1 = [..kvps];

// 混在も需要あり。
Dictionary<string, int> d2 = ["one": 1, ..>kvps];

// 特に、「複数のディクショナリのマージ」みたいな用途で以下のように書きたい。
var kvps1 = new[] { KeyValuePair.Create("three", 3) };
Dictionary<string, int> d3 = [..kvps, ..kvps1];

これも認める方向で検討されています。

検討事項3: ディクショナリじゃなくて KeyValuePair のリスト

[] 中の key: value は「KeyValuePair を作るための簡易記法」みたいなものになっているわけですが、 だったら以下のような「ディクショナリじゃないただのコレクションに対して使えるか」という話が出てきます。

// 「ディクショナリ式」の最有力候補の文法:
List<KeyValuePair<string, int>> d = [
    "one": 1,
    "two": 2,
    ];

これも需要がそれなりにありそうです。 .NET の BCL とか、 Roslyn 中のコードでもオプションとかをディクショナリではなくて IEnumerable<KeyValuePair<TKey, TValue>> 引数で受け取っているものがそれなりにあるそうで。

それに、先ほどの Dictionary<string, int> d3 = [..kvps, ..kvps1]; みたいなもので、マージ元になる kvps などはディクショナリではなくて KeyValuePair のリストということは十分ありそうな話です。

ということで、これも承認されそうです。

検討事項4: KeyValuePair 以外の要素は認められるか

Dictionary<TValue, TKey> とかでは要素の列挙などに KeyValuePair<TValue, TKey> を使うことが多いですが、 ディクショナリ式を作るにあたって KeyValuePair だけに絞るか、それとも他の型も使えるようにするかという問題もあります。

例えば、タプル導入時にも、(TKey key, TValue value) はほぼ KeyValuePair<TKey, TValue> と同等」みたいなことを言われています。 割かし最近 BCL に追加された PriorityQueue なんかは、(TElement Element, TPriority Priority) で要素とその優先度の列挙をします。 Zip なんかも (TFirst First, TSecond Second) で結果を列挙します。 こういうものを直接 [] の中で .. で展開したかったりはします。

あとは、KeyValuePair を特別扱いするとしても、暗黙の型変換を認めるかどうか。

struct Pair<X, Y>(X x, Y y)
{
    public static implicit operator KeyValuePair<X, Y>(Pair<X, Y> pair) => ...;
}

Dictionary<string, int> d = 
[
    new Pair("one", 1),
    .. new[] { new Pair("two", 2) }
];

これについては結論はまだ出ていないみたいです。

検討事項5: Add か、インデクサーか

まず、ディクショナリ式ではキーの重複を認めるかどうかという話があります。 例えば、ToDictionary なんかでは、キーが重複していると例外を出します。

var d = new[] { (1, 10), (1, 20) }
    .ToDictionary(x => x.Item1); // ArgumentException

が、まあ、前述の2個のディクショナリをマージするようなケースでは重複を認める方がよかったりします。 オプション指定とかだと「デフォルト設定と、ユーザーごとの設定をマージ、後で追加した方を優先」みたいな使い方を結構しますし。

ただ、「重複を認めるかどうか」という観点だと、結局は「ターゲットにする型によって挙動が違う」ということになります。 例えば、以下のような感じ。

  • Dictionary<TKey, TValue>Add は重複を認めていない
  • ImmutableDictionary<TKey, TValue>Add は上書き(上書きした新しいインスタンスを作って返す)
  • FrozenDictionary<TKey, TValue>Add (ICollection インターフェイス越しに呼べちゃう) は NotSupported 例外を出す

なので結局は「どういう動作にするか」は決めれなくて、「Add とインデクサーのどちらを使うか」という話になります。

// Add で初期化。
Dictionary<string, int> d1 = new();
d1.Add("a", 1);
d1.Add("b", 2);

// インデクサで初期化。
Dictionary<string, int> d2 = new();
d2["a"] = 1;
d2["b"] = 2;

ちなみにこれらは、現状のコレクション初期化子・オブジェクト初期化子を使うと以下のように書けるやつです。

// Add での初期化になるコレクション初期化子。
Dictionary<string, int> d1 = new()
{
    { "a", 1 },
    { "b", 2 }
};

// インデクサでの初期化になるオブジェクト初期化子。
Dictionary<string, int> d2 = new()
{
    ["a"] = 1,
    ["b"] = 2
};

["a": 1, "b": 2] はどちらになるかという話なわけですが、 現状はインデクサー案が有力みたいです。 コレクション初期化子(Add になる)と食い違うという懸念もありますが、 インデクサーの方が都合がよさどうという判断になっています。 例えば先ほど例に挙げた [..defaultSettings, ..userSettings] みたいなケースで重複を認めている方がよさそうで、 Dictionary<TKey, TValue> の場合は「Add は重複不可、インデクサーは可」ですし。

Extensions (拡張型)

$
0
0

C# 3.0 から拡張メソッドが使えるわけですが、 もうちょっといろんな「拡張」をしたいという話が前々からあります。 例えば以下のような要求。

  • 既存の型に静的メンバーも足したい
  • プロパティや演算子も足したい
  • インターフェイスの後付けもしたい

今では Extensions とか呼ばれていまして、以下の issue でトラッキング中。

ここからさかのぼって、かつては Extension everything とか呼ばれていたり、 個別に「インターフェイスを実装したい」「演算子を拡張したい」など個別の issue がありました。

2015年(Roslyn が GitHub での公開に切り替わった年)にはすでにそんな話が出ています。

結構大きな機能なのでしり込みしていたみたいですが、 去年くらいから Working Group (この機能の追加を推進するメンバーを割り当てて、定期的にミーティング)を設けて作業を始めました。

うちのブログでも去年、1度取り上げています。

もう9年も経ってしまい、C# 12 でも入らなかったわけですが、 ついに今年、C# 13 には一部入りそう(インターフェイスの後付けだけは無理そう)な雰囲気になっています。

最近の話題のうちいくつかを取り上げると、以下のような話が出ています。

extension 構文

ということで、改めて Extensions の話を。 今、以下のような構文を足そうとしています。

// 拡張の構文例。
implicit extension SomeExtension for SomeClass : IEquatable<SomeExtension>
{
    // 追加したいメンバーを書く。

    // 1. 静的メンバーも書ける。
    public static int Y => X * X;

    // 2. メソッド以外も書ける。
    public int Property
    {
        get => GetValue();
        set => SetValue(value);
    }

    public int this[int index] => GetValue(index);

    // 3. インターフェイスの実装を持てる。
    public bool Equals(SomeExtension? other) => Property == other?.Property;
}

// 拡張の対象の例。
class SomeClass
{
    // (中身は適当。)
    public static int X = 123;

    private int _value;

    public int GetValue() => _value;
    public void SetValue(int value) => _value = value;
    public int GetValue(int index) => _value * index;
}

ちなみに、「インターフェイスの実装を持つ」には少し難題があって、 C# 13 時点では入らない可能性がかなり高いです。

普通の構造体 + Unsafe.As

拡張はラッパー構造体を使った実装になりそうです。 一時期は以下のような ref struct を使った実装になりそうだったんですが、 この案は結局没になりました。

var value = new SomeStruct();
var extension = new SomeExtension(ref value);

// 拡張プロパティを呼び出す。
extension.Property = 123;

// ちゃんと元インスタンスに値が反映。
Console.WriteLine(value.GetValue());

ref struct SomeExtension(ref SomeStruct @this)
{
    ref SomeStruct @this = ref @this;

    public int Property
    {
        get => @this.GetValue(); // ref で持ってるので、引数でもらった構造体に書き換えが反映される。
        set => @this.SetValue(value);
    }
}

// デモ用に構造体に変更。
struct SomeStruct
{
    private int _value;

    public int GetValue() => _value;
    public void SetValue(int value) => _value = value;
    public int GetValue(int index) => _value * index;
}

この案に変わって、普通の構造体 + Unsafe.As を使う路線で考えているそうです。

using System.Runtime.CompilerServices;

var value = new SomeStruct();

// Unsafe.As を使って、value 値が入っているの場所を無理やり SomeExtension で解釈。
ref var extension = ref Unsafe.As<SomeStruct, SomeExtension>(ref value);

// 拡張プロパティを呼び出す。
extension.Property = 123;

// extension の参照先が value なので、ちゃんと value が書き変わる。
Console.WriteLine(value.GetValue());

// 普通の構造体。
struct SomeExtension
{
    private SomeStruct @this;

    public int Property
    {
        get => @this.GetValue();
        set => @this.SetValue(value);
    }
}

// SomeStruct は先ほどと同じ。

型消去

Extensions は普通の型と同じように使えたりします。 (特に、explicit を付けた Extensions はむしろ「型を明示しないと使えない」状態になります。) なのでこれを拡張型(extension types)と呼んだりもします。

で、前節の通り Extensions のコンパイル結果はラッパー構造体だったりするわけですが、 このラッパー構造体への変換(Unsafe.As)はあくまでメンバー参照のタイミングで行われます。 メソッドの引数などに拡張型を書くと、実際には「元の型 + 属性」(いわゆる「型消去」方式)になる予定です。 例えば、以下のようなメソッドを書いたとして、

static int Sum(SomeExtension a, List<SomeExtension> b)
{
    var sum = a.Property;
    foreach (var x in b) sum += x.Property;
    return sum;
}

以下のような類のコードに置き換わる予定です。

static int Sum(
    // SomeExtension は属性の中にしか残らない。
    // 元の、 SomeStruct に置き換わる。
    [Extension(typeof(SomeExtension))] SomeStruct a,
    [Extension(typeof(SomeExtension))] List<SomeStruct> b)
{
    // メンバーアクセスするところで Unsafe.As
    var sum = Unsafe.As<SomeStruct, SomeExtension>(ref a).Property;
    foreach (var x in b) sum += Unsafe.As<SomeStruct, SomeExtension>(ref Unsafe.AsRef(in x)).Property;
    return sum;
}

変性を持っていない List<T> で、 List<SomeStruct>List<SomeExtension> に変換する手段は通常全くありません。 型消去で List<SomeExtension>List<SomeStruct> に置き換わることで、 List<SomeStruct> 型の変数を List<SomeExtension> 型の引数に渡せるようになっています。

メンバーのルックアップ(継承)

拡張型は元となる型との間には、クラスの継承関係と似た関係が成り立ちます。 なので、メンバーのルックアップのルールも「クラスの継承に準ずる」で行きたいそうです。 例えば、派生クラスから基底クラスのメンバーを何の修飾もなしで(this. とか base. が必須ではなく)参照できるように、 拡張型から元となる型のメンバーも修飾なしで参照できます。

おさらい的に、「継承があるときのルックアップ」の例をいくつか紹介しておきます。 (拡張型中で元となる型と同名のメンバーを書くとこれに準ずることになると思われます。)

近い側優先:

class Base
{
    public void M(int x) { }
}

class Derived : Base
{
    public new void M(int x) { }

    public void M()
    {
        // 近い側優先なので、Derived.M が呼ばれる。
        M(1);
    }
}

もうちょっとわかりにくい例:

class Base
{
    public void M(int x) { }
}

class Derived : Base
{
    public new void M(object x) { }

    public void M()
    {
        // わかりにくいけども、Derived.M(object) の方が呼ばれる。
        // 引数の型を考えると Base.M(int) が呼ばれそうに見えるけども、そうはならない。
        // (「元々はなかったけど後から Base の方に M(int) が追加された」みたいな状況で破壊的変更にならないようにするため。)
        M(1);
    }
}

メンバーのルックアップ(拡張同士)

あと、既存の拡張メソッドには以下のような優先度があります。

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    class A
    {
        public void M() => Console.WriteLine("Instance");
    }

    class Program
    {
        public static void Main()
        {
            // インスタンス メソッド優先。
            new A().M(); // Instance
        }
    }
}
namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    class A;

    static class AExtension
    {
        public static void M(this A _) => Console.WriteLine("Extension in App1");
    }

    class Program
    {
        public static void Main()
        {
            // 同じ名前空間内の拡張メソッド優先。
            new A().M(); // in App1
        }
    }
}
using Ex1;

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace Ex2
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 内側で using した方優先。
            new A().M(); // in Ex2
        }
    }
}
namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace Ex2
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    using Ex1;
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 優劣がない場合はコンパイル エラー。
            new A().M();
        }
    }
}

新しい拡張型でも同様のルールになると思われます。

一方で、旧「拡張メソッド」と新「拡張型」に優劣をつけるかという議題もありますが、 現状は「優劣つけない」という方向で検討されています。 というか、新旧混在した時点でコンパイル エラーにしようかという話もあるみたいです。

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("old extension method");
    }
}

namespace Ex2
{
    implicit extension AExtension for A
    {
        public void M() => Console.WriteLine("new extension type");
    }
}

namespace App1
{
    using Ex1; // これが外にあってもエラーにする案もあり
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 優劣を付けない(コンパイル エラーになる)。
            // 何なら新旧混在している時点でコンパイル エラーにする可能性濃厚。
            new A().M();
        }
    }
}

インターフェイス実装

ここまでの話は割かし C# 13 で入りそうな話なんですが、 最後に1つ、13では入らなさそうなのがインターフェイス実装の後付けです。

これまでの話どおり、ラッパー構造体を作る方針で少し考えてみましょう。

インターフェイス実装に関する部分だけ残して、以下のようにしたとします。

var value = new SomeClass { Value = 1 };
SomeExtension extension = value;

extension.Equals(new SomeClass { Value = 1 });

explicit extension SomeExtension for SomeClass : IEquatable<SomeExtension>
{
    public bool Equals(SomeExtension? other) => Value == other?.Value;
}

class SomeClass
{
    public int Value;
}

ラッパー構造体で展開するとしたら以下のようになります。

using System.Runtime.CompilerServices;

var value = new SomeClass { Value = 1 };
ref var extension = ref Unsafe.As<SomeClass, SomeExtension>(ref value);

var temp = new SomeClass { Value = 1 };

// こういう風に直接インターフェイス メンバーを呼ぶ分には特に問題なさげ。
extension.Equals(Unsafe.As<SomeClass, SomeExtension>(ref temp));

struct SomeExtension : IEquatable<SomeExtension>
{
    private SomeClass Value;
    public bool Equals(SomeExtension other) => Value.Value == other.Value?.Value;
}

class SomeClass
{
    public int Value;
}

この例はインターフェイス実装しているといっても、そもそもメンバーを直接呼んでいるので問題がないだけです。 問題は以下の状況。

  • インターフェイス型や object 型の変数で受けてボックス化する場合
  • ジェネリック メソッドに渡す場合

まず、インターフェイス型の変数で受けてみましょう。 ReferenceEqualsis 判定であまり期待通りとは言えない挙動を起こします。

using System.Runtime.CompilerServices;

var value = new SomeClass { Value = 1 };
ref var extension = ref Unsafe.As<SomeClass, SomeExtension>(ref value);

// インターフェイスに渡そうとすると、この実装だとボックス化が発生。
IEquatable<SomeExtension> boxedExtension = extension;

// インスタンスが一致しなくなる。
Console.WriteLine(ReferenceEquals(value, boxedExtension)); // false

// ダウンキャストが失敗する。
Console.WriteLine(boxedExtension is SomeClass); // false

ジェネリク メソッドでは、以下のように、元の型と拡張型の両方の型情報を使う必要がでてきます。

var value = new SomeClass { Value = 1 };
List<SomeClass> list = [new() { Value = 2 }, new() { Value = 1 }, new() { Value = 0 }];

// SomeClass のままだと IEquatable 制約を満たさなくて呼べない。
var i1 = IndexOf<SomeClass>>(list, value);

// これなら呼べるようになるはず。
// ただ、list は List<SomeClass> なので、やっぱり型消去が必要。
// 型引数が暗黙的に SomeClass と SomeExtension の2つに増えるような処理が必要。
var i2 = IndexOf<SomeExtension>(list, value);

static int IndexOf<T>(List<T> list, T value)
    where T : IEquatable<T>
{
    // 今の型システムだと T が通常の型か拡張型かを知るすべはなく、Unsafe.As 展開ができない。
    for (int i = 0; i < list.Count; i++)
        if (list[i].Equals(value))
            return i;
    return -1;
}

いずれも、C# コンパイラー上のトリックでは問題を解消できなさそうで、 .NET ランタイムの型システムに手を入れる必要が出てきそうです。 型システムに手を入れるとなると結構大ごとなので、C# 13 で実現する見込みは残念ながらほぼありません。

Lock クラス

$
0
0

今日は、 .NET 9 で Lock クラスというのが入る予定で、 それに伴って C# コンパイラーにも対応が必要そうという話。

一応雰囲気的には C# 13 に入りそう。

任意のオブジェクトを lock

C# はなぜか任意のオブジェクト インスタンスを使って排他制御ができます。 ロックを掛けるために以下のようなコードを書くことになります。

class MultiThreadCode
{
    private readonly object _syncObj = new object();

    public void Run()
    {
        lock (_syncObj)
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }
}

Java からの習慣(= 1995年頃の発想)ですかね。 Java の synchronized ブロックも同じ仕様のはず。

本来の思想としては「lock()() 内には同時に操作されるとまずいリソースを書く」という感じのはず。 そういわれると、lock (任意のオブジェクト) に正当性があるように感じます。

class Resource;

class MultiThreadCode
{
    private readonly Resource _someResource = new();

    public void Run()
    {
        lock (_someResource)
        {
            // _someResource に対する操作をする。
            // _someResource を同時に操作されると困るんだから、「_someResource を lock」。
        }
    }
}

ですがまあ、実際のところこんなにきれいに lock (x) { x に対する操作 } になることはなく、 大体は先ほどのように「lock のためだけに1個追加で object _syncObj みたいなフィールドを用意」みたいなことになります。

これがめんどくさく… とはいえ、面倒だからといって以下のようなことはしてはいけないとされています。

class MultiThreadCode
{
    public void Run()
    {
        // ✖
        // 任意のオブジェクトでロックできるということは、this でも行ける!
        lock (this)
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }

    public static void StaticRun()
    {
        // ✖
        // 静的メソッド内では this がない…
        // そうだ、Type 型もオブジェクトじゃん!
        lock (typeof(MultiThreadCode))
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }
}

「外に漏れるインスタンスでロックを取ってはいけない」というお作法があるからです。 以下のようなコードを書かれる可能性があって困ります。

var x = new MultiThreadCode();

// ここの lock と、MultiThreadCode.Run 内の lock (this) が同じオブジェクトをロックする。
// 意図しない挙動のはず。
lock (x)
{
}

class MultiThreadCode
{
    public void Run()
    {
        lock (this)
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }
}

さらにいうと、外に漏れてダメなら以下のようなコードもダメになると。

var x = new MultiThreadCode();

// ここの lock と、MultiThreadCode.Run 内の lock (_items) が同じオブジェクトをロックする。
lock (x.Items)
{
}

class MultiThreadCode
{
    // private だから一見外に漏れてない。
    private readonly List<int> _items = [];

    public void Run()
    {
        lock (_items)
        {
            // _items に Add/Remove とかしたり。
        }
    }

    // List としては公開していないものの、
    // インスタンス自体は _items そのままなので…
    public IEnumerable<int> Items => _items;
}

なのでまあ、元の話の戻りますが、結局は「_items とは別に object _syncObj = new(); を用意」みたいなことになります。

.NET のオブジェクト ヘッダー

「任意のオブジェクトに対して lock を掛けれるという仕様は意外とオーバーヘッドが大きい」という話題があったりします。 なので、「ロック専用のクラスがあった方がいい」という話も。

ここにこんな説明があります:

Locking on any class has overhead from the dual role of the syncblock as both lock field and hashcode et al.

(任意のクラスに対するロック操作は、ロック用の値とハッシュ値とか、syncblock に複数の役割を持たせていることによるオーバーヘッドを持つ。)

syncblock が何かという話は以下の英語の記事がわかりやすそう。

ここの図を見ての通り、27ビット目の値によって、下位ビットをハッシュ値として使うか、ロック用に使うか分岐させています。

ところがまあ、これのせいで分岐予測をミスりまくって、結構ペナルティになるみたいです。 言われてみればそりゃそう。 GetHashCodelock だったら GetHashCode の方が圧倒的に利用頻度高いでしょうから。 いざ lock しようとすると分岐予測当たらないのもしょうがなく。

(あと、lock 中のオブジェクトに対して override してない object.GetHashCode を呼ぶと遅くなります。)

で、ここで、前節の「どうせロック専用のインスタンスを作ることが多い」話と合わせると、 「だったらロック専用の Lock クラスを作って private readonly Lock _syncObj = new(); しようよ」ということになったりします。

System.Threading.Lock クラス

ということで、 .NET 9 では Lock クラス(System.Threading 名前空間)を追加するみたいです。 現状 (.NET 9 Preview 1 とか Preview 2 時点)では、 プレビュー扱いで RequiresPreviewFeatures 属性が付いていますが、 一応今でも実装が入っています。

C# の lock ステートメントをどうするかはいったん置いておいて(後述)、 以下のような使い方を想定しているクラスです。

using System.Runtime.Versioning;

// 今のペースなら、.NET 9 正式リリースまでには外れる気はする。
[module: RequiresPreviewFeatures]

class MultiThreadCode
{
    private readonly Lock _syncObj = new();

    public void Run()
    {
        // C# コンパイラーに手を入れないとしたらこんな使い方に。
        // lock じゃなくて using。
        using (_syncObj.EnterScope())
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }
}

Lock クラスが何をやっているかというと、おおむね「lock が内部で使っている C++ コード(AwareLock)を C# に移植」です。 本当に、「オブジェクト ヘッダーの syncblock を使うのが高コスト」を避けるためのクラスという感じです。

lock ステートメントで Lock インスタンス

ここで問題になるのが、じゃあ、Lock インスタンスに対して lock ステートメントを使うとどうなるの?というお話。 「Lock の時には lock (x) じゃなくて using (x.EnterScope()) にしようね」とか言われても割と困るかと思います。 知らなきゃ確実に lock (x) と書くでしょうし、 知ってたって lock (x) をやらかす自信があります。

なので、C# 言語のレベルでも何らかの対処は必要だろうという話になります。 (おそらくその辺りが RequiresPreviewFeatures 属性付きになっている理由。)

検討段階では「lock (x) すると警告を出すみたいなのだけでもいいかもしれない」なんて話もありましたが、 まあ、「lock (x) と書いたらコンパイラーが using (x.EnterScope()) に置き換える」路線で行くことになりました。

この実装、 Visual Studio 17.10.0 Preview 2.0 (3週間くらい前)の時点で入ってるみたいです。 以下のコードを書いて、ILSpy とかでコンパイル結果の中身を覗くと using (_syncObj.EnterScope()) に置き換わっています。

class MultiThreadCode
{
    private readonly Lock _syncObj = new();

    public void Run()
    {
        // C# コンパイラーが特殊対応することになったので、lock で OK に。
        lock (_syncObj)
        {
            // いろんなスレッドから同時に呼ばれるコード。
        }
    }
}

ちなみに、現状は Lock クラス専用です。 珍しくパターン ベースでなく、Lock でないと認識せず。 まあ、需要がないんでしょうね。

// これは現状、既存の lock (Monitor.TryEnter を使ったコード)になる。 
lock (new MyLock())
{
}

// System.Threading.Lock と同じパターンのメソッド持ちの自作クラス。
class MyLock
{
    public Scope EnterScope() => default;

    public ref struct Scope
    {
        public void Dispose() { }
    }
}

ref/ref struct 変数を非同期メソッド中で使えるように

$
0
0

前回の Lock クラスの話を見てから、とりあえず以下のコードを見てほしい。

using System.Runtime.Versioning;

[module: RequiresPreviewFeatures]

class MultiThreadCode
{
    private static readonly object _syncObj = new();
    private static readonly Lock _syncLock = new();

    public static IEnumerable<object?> MIterator()
    {
        lock (_syncObj) { } // 旧来 lock。
        lock (_syncLock) { } // 新しい lock (VS 17.10p2 以降)。

        yield return null;
    }

    public static async ValueTask MAsync()
    {
        lock (_syncObj) { }
        lock (_syncLock) { } // これだけダメ(VS 17.10p2 以降)。

        await Task.Yield();
    }
}

おそらく C# 13 正式リリースまでには直ると思うんですが、 どうしてこうなるのかと、どう対処する予定なのかという話になります。

ちなみに、単に Lock クラスに対して特殊処理をするという話ではなく、 もう少し汎用に「非同期メソッド中で ref ローカル変数を使えるようにする」という対処になります。

lock の展開

[前回の話]で、今回関係するのは、Lock インスタンスに対する lock ステートメントが using (x.EnterScope()) み化けるという点。 で、さらにいうと、using は以下のように展開されます。

class MultiThreadCode
{
    private static readonly Lock _syncLock = new();

    // 元コード。
    public static void A()
    {
        lock (_syncLock) { }
    }

    // lock → using。
    public static void B()
    {
        using (_syncLock.EnterScope()) { }
    }

    // using → try-finally。
    public static void C()
    {
        Lock.Scope scope = _syncLock.EnterScope();
        try
        {
        }
        finally
        {
            scope.Dispose();
        }
    }
}

ここで、Lock.Scoperef struct になっています。 これが先ほどのコードで非同期メソッド中の lock (_syncLock) がエラーになる原因です。 問題の本質としては以下のようなコードと同じ。

class A
{
    public static IEnumerable<object?> MIterator()
    {
        // イテレーター中では ref strcut を使える。
        // (ただし、yield をまたがない場合のみ。)
        Span<int> span = stackalloc int[1];

        yield return null;
    }

    public static async ValueTask MAsync()
    {
        // こちらはダメ。
        Span<int> span = stackalloc int[1];

        await Task.Yield();
    }
}

イテレーターと非同期メソッドって、仕組みがかなり似ていて、「イテレーターでできて非同期メソッドでできない」ということは原理的にはあまりないんですが。 実際、上記の挙動は単に実装都合で、コストさえかければ「非同期メソッド中でも ref struct のローカル変数を書けるようにする」というのは可能です。

イテレーターの中断と再開

イテレーターのコンパイル結果」辺りで書いてるんですが、 イテレーターは「中断と再開」をするようなコードが生成されます。

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

foreach (var x in M())
{
    Console.WriteLine(x);
}

IEnumerable<int> M()
{
    var x = 1;
    yield return x * x;

    // 式は適当。
    // ここで重要なのは、y は yield をまたがないということ。
    var y = ++x * x;
    y *= y;

    yield return y;

    // 同、z は yield をまたがない。
    var z = ++x;
    z *= (2 * x + 1);

    yield return z;
}

おおむね、以下のようなクラスが生成されます。 (簡単化のためちょこっとさぼっています。要点のみ。)

var e = new MImpl();
while (e.MoveNext())
{
    Console.WriteLine(e.Current);
}

class MImpl
{
    private int _i = 0;
    private int _x = 1;

    public int Current { get; private set; }

    public bool MoveNext()
    {
        if (_i == 0)
        {
            Current = _x * _x;
        }
        else if (_i == 1)
        {
            var y = ++_x * _x;
            y *= y;
            Current = y;
        }
        else if (_i == 2)
        {
            var z = ++_x;
            z *= (2 * _x + 1);
            Current = z;
        }
        else
        {
            return false;
        }

        _i++;
        return true;
    }
}

ここで重要なのは以下の点。

  • yield をまたいで使う変数はフィールドに昇格する
  • そうでないものはローカル変数のまま

つまり、「yield さえまたがなければ、ローカル変数に制限を掛ける必要はない」ということになります。 ここではイテレーターで話しましたが、非同期メソッドもほぼ同様で、 「await さえまたがなければ、ローカル変数に制限を掛ける必要はない」といえたりします。

ただまあ、これはあくまで「原理的には」という話であって、じゃあ、現在の実装がどうなっているかというと… C# 12 時点では以下のような感じ。

class A
{
    public static void M()
    {
        RefStruct rs = new();

        using (rs) { }
        foreach (var _ in rs) ;

        int x = 1;
        ref int r = ref x;
    }

    public static IEnumerable<object?> MIterator()
    {
        RefStruct rs = new();

        using (rs) { }
        foreach (var _ in rs) ; // ダメ。

        int x = 1;
        ref int r = ref x; // ダメ。

        yield return null;
    }

    public static async ValueTask MAsync()
    {
        RefStruct rs = new(); // 非同期メソッドだとこの時点でダメ。

        using (rs) { } // ダメ。
        foreach (var _ in rs) ; // ダメ。

        int x = 1;
        ref int r = ref x; // ダメ。

        await Task.Yield();
    }
}

ref struct RefStruct
{
    public void Dispose() { }

    public RefStruct GetEnumerator() => this;
    public int Current => 0;
    public bool MoveNext() => false;
}

どれも、「ref struct のローカル変数が認められるのであれば書けてもいいはずのコード」になります。 ところが、大丈夫なものとコンパイル エラーになるものがまちまち。

ref/ref struct 変数を非同期メソッド中で使えるように

まあ既知の問題ではあったんですが。 これまで、需要がそこまでないからか、ずっと放置されていました。 ところが、今回「Lock クラスに対する lock ステートメント」問題が出たからか、急に対処することになったみたいです。

先ほどの、以下のようなコード、すべて「yield/await さえまたがなければ認める」ということになりそうです。

RefStruct rs = new();

using (rs) { }
foreach (var _ in rs) ;

int x = 1;
ref int r = ref x;
  • ref ローカル変数
  • ref struct のローカル変数
    • ref struct に対する using ステートメント
    • ref struct に対する foreach ステートメント

付随して、同じく「yield/await さえまたがなければ認める」という条件で、 unsafe ブロックも認めるそうです。

lock 中の yield

逆に、「これまで書けちゃっていたけども、実はまずかった」というものに警告を出そうという話もあります。 それが「lock ステートメント中の yield」です。

class MultiThreadCode
{
    private static readonly object _syncObj = new();

    public static IEnumerable<object?> MIterator()
    {
        lock (_syncObj)
        {
            // これが書けちゃう。使い方によってはまずい。
            yield return null;
        }
    }

    public static async ValueTask MAsync()
    {
        lock (_syncObj)
        {
            // 非同期メソッドの場合、コンパイル エラーになるので大丈夫。
            await Task.Yield();
        }
    }
}

.NET の実装では、 「ロックの開始と終了(内部的には Monitor.EnterMonitor.Exit)は同じスレッドでやらないといけない」という制限がありまして。 非同期メソッドの方はわかりやすく「await をまたぐと別スレッド」感があるのでコンパイルの時点でエラーにしています。

で、イテレーターの方も使い方によっては「yield をまたぐと別スレッドになることがある」という意味では危険で、 例えば、以下のようなコードを書くと実行時に SynchronizationLockException 例外が出ます。

object syncObj = new();

IEnumerable<object?> M()
{
    lock (syncObj)
    {
        // これが書けちゃう。使い方によってはまずい。
        yield return null;
    }
}

foreach (var _ in M())
{
    // M 内に非同期コードがなくても、利用側が非同期だった時点でアウト。
    await Task.Yield();
}

ということで、この「lock 中での yield」も警告を足すことになりそうです。 (いきなりエラーにすると破壊的変更になるのでとりあえず警告。 何バージョンかかけてエラーに変更する可能性はあり。)

(※ 「スレッドをまたいだ lock を書けるようにする」みたいなことはしません。)

.NET 9 の破壊的変更の1つを踏んだ話

$
0
0

かなりのレアケースを踏んだので酒の肴程度にその話を。

破壊的変更の内容: 浮動小数点数 → 整数の飽和変換

破壊的変更の告知ページ:

最小再現コードは以下の通り。

var x = int.MaxValue;
var y = (float)x;
var z = (int)y;
Console.WriteLine(z);

z の値は、 .NET 8 では -2147483648 (int.MinValue) になって、 .NET 9 では 2147483647 (int.MaxValue) になります。

(注意: float の精度の問題で、y の値は int.MaxValue よりも大きい扱いを受けていそうです。 double では2行目を var y = (double)x + 1; にすると再現。)

int の範囲に収まらない float の値を int に変換した時の挙動が変わりました。

  • 古い挙動: x < int.MinValue もしくは x > int.MaxValue のとき、(int)xint.MinValue になる
  • 新しい挙動: x < int.MinValue のとき (int)xint.MinValuex > int.MaxnValue のとき (int)xint.MaxValue

どう見ても新しい挙動の方が自然…

破壊的変更をした理由

変な挙動であっても、変更するメリットがそれなりにないと破壊的変更が認められることはほとんどありません。

今回の場合何があったのかというと、AVX512 命令を使いたかったということみたいです。

AVX512 には double, floatint, uint とかに変換するための vfixupimmsd などの命令があるそうで。 ハードウェア命令を持っているんなら、ソフトウェア計算するよりもこの命令を使った方がパフォーマンスがよくなります。 当然、積極的に活用したいんですが、どうも、この命令の挙動が前節の「新しい挙動」になるみたいです。

「AVX512 命令が使える場合だけ挙動が変わる」みたいなことになるとかえってまずいので、だったら、 AVX512 が使えないとき向けのソフトウェア実装の挙動も改めて、 破壊的変更の告知を出してしまおうという流れに。

破壊的変更の踏み方

int.MaxValue を超える値を int に変換して使う」とか普通はやらないし、 「こんなの影響する人いないでしょ」と高を括っていたものの…

自社のコードに .NET 8 から 9 に変更したら永久ループを起こすコードがありました。 すごく簡素化して書くと以下のようなコードがあったせい。

// .NET 9 でだけ永久ループ…
M((int)Math.Floor(float.MaxValue), (int)Math.Floor(float.MaxValue));

Console.WriteLine("done.");

static void M(int x, int y)
{
    // y が int.MaxValue だと、i++ がオーバーフローして永久ループになる。
    for (int i = x; i <= y; i++) ;
}

.NET 8 では、M の引数に渡る値は int.MinValue でした。 それが、.NET 9 になると int.MaxValue に変わります。

で、以下のコードは「for の中を1回だけ実行」になりますが、

for (int i = int.MinValue; i <= int.MinValue; i++) ;

以下のコードは永久ループです。

// i++ がオーバーフローするので i <= int.MaxValue が false になることはない。
for (int i = int.MaxValue; i <= int.MaxValue; i++) ;

背景としては以下のような感じ。

  • record struct Point(float X, float Y) で座標管理している
  • 「無効な座標」が必要になったものの、工数的に Point? に書き換える余裕はなかった
  • 「すごく遠い座標」を与えてみて、テストプレイしてみたところ所望の動作になったので、float.MaxValue の座標に飛ばしてた
  • 経路探索ロジックが、パフォーマンスのため、内部的に整数で計算していた
  • for (int i = x; i <= y; i++)(int)float.MaxValue が来る
  • for は、iint.MinValue だったら何も起きないようなコードだった(ループ内を1回通ってたけども特に問題は起きなかった)

だいぶレアな不幸が重なった感じ…

以下の条件が重ならないと起きないですからね。

  • float.MaxValue (int.MaxValue を超える値)を特殊な意味に使っちゃった
  • <= でループしてる
  • int.MinValue のときにはたまたま問題を起こさないコードだった

問題を特定できた時、かなりびっくりしました。


(没) UTF-8 文字列補間

$
0
0

今日のは、C# 言語機能としては否決されたものの、ほぼ同等のものがライブラリと JIT 時最適化で実現されたという話になります。

ちなみに今日のこの話は .NET 8 の頃の話で、「そういえば去年書いてなかった」ネタになります。

UTF-8 リテラルがあるなら

C# 11 で UTF-8 リテラルが入って、 C# プログラム中に UTF-8 なバイト列を ReadOnlySpan<byte> で直接埋め込めるようになりました。

ReadOnlySpan<byte> hex = "0123456789ABCDEF"u8;

割かし何度も書いてたりはしますが、 もう今となっては世の中の多くの文字列が UTF-8 でやり取りされています。 .NET の string が UTF-16 ベースなので、UTF-16 ⇔ UTF-8 の変換がそれなりのコストになっていたりします。

そこで、いろんなものを直接 UTF-8 で書き込んだりするようになりました。 今、 .NET のプリミティブ型にも IUtf8SpanFormattable インターフェイスとかが実装されていて、UTF-8 文字列化を直接できるようになっています。

そうなると欲しくなるのが UTF-8 に直接書き込める文字列補間。 以下のようなことをできるといいなぁという要望があります。

static byte[] Format(int x, int y) => $"(X: {x:X2}, Y: {y:X2})"u8;

要は、文字列補間の $"" にも u8 を付けて、直接 UTF-8 を書き込むというもの。

この提案、一瞬本気で検討はされてたんですけども:

結論がどうなったかというと、「普通の文字列補間を使って、パフォーマンスを落とさずに UTF-8 補間できるように JIT 最適化したからいいや」という感じで、C# 言語機能は不要ということになりました。

Utf8.TryWrite

前節の UTF-8 文字列補間(案)に類することをやるために、 .NET 8 移行、以下のような書き方ができます。

using System.Text.Unicode;

static byte[] Format(int x, int y)
{
    var temp = (stackalloc byte[64]);

    Utf8.TryWrite(temp, $"(X: {x:X2}, Y: {y:X2})", out var written);

    return temp[..written].ToArray();
}

Utf8 クラスの TryWrite メソッドを使って直接 UTF-8 なバイト列を作れます。

ただ、文字列補間の部分は普通の $"" で書きます。 そして、この文字列補間の展開結果は以下の通り。

using System.Text.Unicode;

static byte[] Format(int x, int y)
{
    var temp = (stackalloc byte[64]);

    Utf8.TryWriteInterpolatedStringHandler handler = new(9, 2, temp, out var shouldAppend);

    if (shouldAppend
        && handler.AppendLiteral("X: ")
        && handler.AppendFormatted(x, "X2")
        && handler.AppendLiteral(", Y: ")
        )  handler.AppendFormatted(y, "X2");

    Utf8.TryWrite(temp, ref handler, out var written);

    return temp[..written].ToArray();
}

定数文字列の最適化

前節、UTF-8 補間に、普通の文字列補間構文を使っているため、 AppendLiteral("X: ") とかで、普通の文字列リテラル(UTF-16)が発生しています。 当然ここで「そんなことしたら UTF-16 から UTF-8 への変換コストが発生したりするんじゃないか?」という話になるんですが、 そこが今日の本題。 .NET 8 移行、定数文字列の最適化がものすごい進んだみたいでして。 結果、この変換コストはほとんど発生しないそうです。

AppendLiteral の中身はこんな感じ:

内部的に ReadUtf8 ってのを呼んでるんですけども、これはこちら:

説明文に、

Same as Encoding.UTF8.TryGetBytes, except with refs, returning the number of bytes written (or -1 if the operation fails), and optimized for a constant input.

(Encoding.UTF8.TryGetBytes とほとんど一緒で、違いは、ref を使っていて書き込んだバイト数を返すのと、定数入力に対して最適化される。)

と書かれていますし、Intrinsic 属性が付いています。 この属性が付いているメソッドは「JIT が特別な最適化したものに置き換える」というマーカーでして、 定数文字列の UTF-16 → UTF-8 変換は JIT がやっちゃいます。 この最適化の Pull Request は以下の通り:

これのおかげで、 AppendLiteral("X: ") のコストがほぼ AppendLiteral("X: "u8) (みたいな ReadOnlySpan<byte> 引数オーバーロードを足すの)と同レベルになるそうで。

$""u8 要る?

そこで本題に戻りまして、UTF-8 文字列補間用の構文、すなわち、$""u8 みたいなものは必要かどうか。

ここまでで説明したように、Utf8.TryWrite(temp, $"(X: {x:X2}, Y: {y:X2})", out var written); みたいに普通の文字列補間(C# のコンパイル結果的には UTF-16)で書いても、JIT 時最適化で UTF-16 から UTF-8 の変換コストがかなり低コストになっています。

この事実を踏まえて、改めて C# コンパイラー チーム内で検討した結果、 backlog (未処理・未完成品、C# チーム用語的には「よっぽど斬新なアイディアが出てこない限りやらない」)行きになりました。

まあ、確かに、Utf8.TryWrite で事足りた感じはあります。

(没) 数学準拠な剰余演算子

$
0
0

こないだにつづき、C# 言語機能としては没ネタ。 最終的な結論が「ライブラリでやれ」 → 「.NET 10 でメソッド追加を検討」です。

剰余の利用例

剰余演算(C# だと % 演算子)の用途として、 「配列の範囲内に収めるために index % array.Length する」とかがあると思います。 例えば以下のような感じ。

var table = new Table([1, 2, 3, 4, 5]);

for (var i = 0; i < 5; i++)
    Console.WriteLine(table.Next());

// 引数で受け取った配列の中身を1個ずつ返す。
struct Table(int[] values)
{
    private int _index;

    public int Next()
    {
        var i = _index;
        var x = values[i];
        _index = (i + 1) % values.Length; // % Length することで範囲内に収める。 
        return x;
    }
}

この状態なら、単純に +1 でやっていて、 i が常に0以上なので特に問題は起きません。

で、これにちょこっと、「ステップ幅」みたいなのを足したとします。 インデックスを何個飛ばしするか。

var table = new Table([1, 2, 3, 4, 5], 3);

// 3個飛ばしにしたので 1, 4, 2, 5, 3 と出力される。
for (var i = 0; i < 5; i++)
    Console.WriteLine(table.Next());

struct Table(int[] values, int step)
{
    private int _index;

    public int Next()
    {
        var i = _index;
        var x = values[i];
        _index = (i + step) % values.Length; // +1 を +step に変更。 
        return x;
    }
}

そしてうっかり、step を負にして IndexOutOfRange

var table = new Table([1, 2, 3, 4, 5], -1);

// IndexOutOfRange で止まる。
for (var i = 0; i < 5; i++)
    Console.WriteLine(table.Next());

struct Table(int[] values, int step)
{
    private int _index;

    public int Next()
    {
        var i = _index;
        var x = values[i];
        _index = (i + step) % values.Length; // 負 % 正 の結果は負です。 
        return x;
    }
}

「負 % 正」の結果は負になるので、values[i] が例外を起こすようになります。

何か多少気持ち悪いですよね。 この類の (i + step) % Length って、 意味的には「配列の末尾を超えたら先頭に戻る」なわけです。 だったら逆に、「配列の先頭を超えたら末尾に戻る」もあってもいいわけで。 この例(Length が5)でいうと、values[0] の次は values[4] に行ってほしい。

剰余の符号

前節みたいな動作になるのは、「大体の CPU の剰余命令がそういう実装だから」です。 C# を始めとして、そこそこパフォーマンスを気にするプログラミング言語では、だいたい「ハードウェア的に一番速い奴を選ぶ」な方針になります。

そもそも、整数の剰余自体、ちょっと定義がぶれていまして。 「以下の条件を満たす」というところまではほとんどの環境で合意が得られてる感じではあるんですが:

a を n で割った商(quotient )を q、余り(remainder )を r として、

  • a = nq + r
  • |r| < |n|

商 q は (実数で計算した場合の) a ÷ n に近い整数ではあるんですが、丸め方によって値が1ずれます。 その結果、余り r の符号も変わります。

例えば、以下のコードで得られる qr はどの丸め方でも必ず a == n * q + rAbs(r) < Abs(n) の条件を満たします。

// 一度 double で計算して、商の丸め方式をいろいろ変えてみる。
// 余りは常に r = a - q * n で計算。
static (int q, int r) DivRem(int a, int n, MidpointRounding mode)
{
    var q = (int)Math.Round((double)a / n, mode);
    var r = a - q * n;
    return (q, r);
}

一応、これでどういう結果が得られるかも例示しておきます:

ShowDivRem(5, 3);
ShowDivRem(-5, 3);
ShowDivRem(5, -3);
ShowDivRem(-5, -3);

static void ShowDivRem(int x, int y)
{
    Console.WriteLine($"DivRem({x}, {y})");
    Console.WriteLine($"  to 0  {DivRem(x, y, MidpointRounding.ToZero)}");
    Console.WriteLine($"  floor {DivRem(x, y, MidpointRounding.ToNegativeInfinity)}");
    Console.WriteLine($"  ceil  {DivRem(x, y, MidpointRounding.ToPositiveInfinity)}");
    Console.WriteLine($"  away  {DivRem(x, y, MidpointRounding.AwayFromZero)}");
}
DivRem(5, 3)
  to 0  (1, 2)
  floor (1, 2)
  ceil  (2, -1)
  away  (2, -1)
DivRem(-5, 3)
  to 0  (-1, -2)
  floor (-2, 1)
  ceil  (-1, -2)
  away  (-2, 1)
DivRem(5, -3)
  to 0  (-1, 2)
  floor (-2, -1)
  ceil  (-1, 2)
  away  (-2, -1)
DivRem(-5, -3)
  to 0  (1, -2)
  floor (1, -2)
  ceil  (2, 1)
  away  (2, 1)

C# の整数の /% (= 大体の CPU の剰余命令の結果)は、この例でいうと ToZero と同じです。 例を見ての通り、余り rの符号は被除数 a と一致します。

一方、前節みたいなものを含め、アルゴリズム的に使いやすいのは ToNegativeInfinity (floor)かもしれません。 正負反転のタイミングで不連続がないのと、r が正負問わず 0~n の範囲に収まるので。

ちなみに、数学ではユークリッド除法を根拠として、

  • 余り r が常に正になるようにする
  • それに合わせて q を (a - r) ÷ n で計算

とすることが多いそうです。

没案: %% 演算子

いろんな剰余があり得る」という話をしたところで、 最初の例に戻りましょう。 最初の例では、要するに「(i + step) % Length の結果は正が良かったな」という話になります。

ということで出てきたのが以下の提案。

ここでいう canonical (聖典・規範に準ずる)というのは、「数学の定義に準ずる」みたいな意味で言っていて、 前節の「ユークリッド除法を根拠にした、余り r を常に正する」方式です。 この定義の余りを返す演算子として %% 演算子を足すのはどうかという案。

結果的にリジェクトされてるんですけども、理由はまあ、前節の説明の後だとわかりやすいですね。 以下のようなことを考えた時、本当に演算子として追加するのが望ましいのかどうかというと微妙。

  • 剰余にもいろいろある
    • 例えば今の場合、Length が正なことはわかっているので、ToNegativeInfinity 丸めでもいい
  • 商と余りはセットで考えないとダメ
    • %% と対になる除算も必要?

まあ、言語組み込みの演算子ではなく、 ライブラリ提供のメソッドでやるべきでしょうね。 以下の方法で上記の問題は解決できるので。

  • DivideRemainderDivRem メソッドをセットで提供
  • 丸め方式は引数で明示

DivRem メソッド

ということで、dotnet/runtime 側に以下の提案が出ています。

現状の案では以下のようなメソッドの追加になります。

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>
{
    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);
}

おそらくこれらは .NET 10 で入ります。 (デザイン レビューで承認済みで、すでに実装作業も始まって Pull Request が出ている状態。)

UTF8 か Utf8 か

$
0
0

今日は C# 配信をやっててちょくちょく話題になるやつの話。

using System.Text;
using System.Text.Unicode;

var buffer = (stackalloc byte[3]);
Utf8.FromUtf16("abc", buffer, out var r, out var w);
Encoding.UTF8.GetString(buffer[..w]);

Utf8 なの? UTF8 なの?

(昔1回同じ話題でブログ書いた気がしつつ、最近もまた話題に出たので。)

.NET の命名ガイドライン

.NET には命名規約に関するガイドラインがありまして、以下の場所にドキュメントとして残っています。

おおむね以下のようなルール。

  • クラス名などは PacalCase を使ってください
    • (2文字よりも長い) 頭文字略語も同様です
    • 2文字の頭文字略語(two-letter acronyms)は例外で、全て大文字

つまるところ、 Utf8UTF8 なら、前者の Utf8 が正解。

ちなみに、 Encoding.UTF8 に何か崇高な理由があるわけではなく、 単純に「初期はルールが徹底されてなかった。すまん。」という懺悔あり。

3文字略語で他に変なのがないかはちょっと検索してみたんですけども、UTF8 以外、自分には見つけられず。 ただ、何せ Encoding.UTF8 の利用頻度が高く、どうしても目立ってしまいます。

特殊ルールやめてほしい問題

例外であるところの「2文字の略語」に該当するのは例えば以下のような名前。

個人的な意見としては、正直、こんな特殊ルールはない方がよかったんじゃないかなぁと思っています。 「System.IO で懺悔案件やらかしちゃったのをあとからつじつま合わせの特殊ルール足しただろ」とか陰口言われてるのも見たことがあります (個人的にはこれに1票)。

System.IoSystem.Gc は確かになんか字面の違和感すごいですし、Il (L が小文字)の視認性の悪さもすごいんですけど、特殊ルールが招く混乱よりはだいぶマシというお気持ち。

↓とか結構キモいと思うんですよねぇ。

  • Avx512BW (AVX-512 の byte and word 命令)
  • Avx512Vbmi (同、Vector Byte Manipulation Instructions)

ルール上は確かに、 BW は全部大文字で、VBMI は先頭だけ大文字なんですけども…

.NET Runtime の中の人もしばしば混乱していまして、 public なものにこそあまりないものの、internal/private なものだとときどき「2文字略語だけど2文字目が小文字」があります。

  • DiyFp (floating point)
  • NtDll (Windows NT の NT。New Technology?)

他に、「.NET Runtime 内のもの(DBNull)は DB だけど、ASP.NET チームが書いてるもの(DbContext)は Db」なんて事案も。 ASP.NET チームは「しがらみはリセット」路線で、やっぱ特殊ルールを快く思ってないんでしょうね。

ID? OK?

PlatformID

こいつだけが「2文字とも大文字の ID」です。 ManagedThreadIdとかEventIdとかは Id。 これも懺悔案件でしょうね。PlatformID は最初期からある enum なので。

何だったら、ドキュメントには↓とか書かれてますからね。IDId に渡す。

PlatformID の列挙型メンバーの整数値を SignTool.exe の PlatformId 引数に渡せる

しかし、id とは一体何なのか…

  • identity の略? → ガイドライン的には「略語は使うな」なので、Identity と書くべき
  • id という英単語(元は identity だったとしても、もう id のスペルで普及)? → 略語ではないので Id と書くべき
  • identity document (本人確認書類)の略? → なら ID
    • PlatformID がこの意味とは考えにくい…

他に、OK もぶれがちで、 OKOkもあります(これも、後者は ASP.NET)。

ok とは一体何なのか…

  • 元々 all correct がなまったスラングの oll korrect の略らしい → OK
  • が、語源ももうあいまいで ok という英単語化してる → Ok
  • なんなら okay というスペルも定着してる → Okay (略すな危険)

この辺りで悩むことになるのも「2文字略語は特別」とかやっちゃったからで、 この特殊ルールがなければ迷うことなく Id, Ok なんですけども。

まとめ

ガイドライン上、Utf8 が正しくて、UTF8 は過去のしがらみです。 ガイドラインは以下の通り。

  • 3文字以上の場合は略語も含め、PascalCase
  • 2文字の略語だけ例外で IO とか GC とか全部大文字

ただ、2文字略語の特殊ルールも割と過去のしがらみ感があって、 ASP.NET とかは2文字の略語も含めて PascalCase を採用しているみたいです。

field キーワード

$
0
0

「Rosly の Language Feature Status にこの1・2か月で結構更新かかったね」という話題もたびたびあり、その辺りの話を。

Language Feature Status に並んでいるもののうち、いくつかは preview として現時点でもうすでに取り込まれています。

  • field キーワード ← 今日はこれ
  • First-class Span
  • nameof(T<>)

今(執筆時、Visual Studio 17.13.0 Preview 2.1)の時点でも、 LangVersionpreview を指定すれば利用可能です。

最初は3つまとめて1ブログにしようかと思ってたんですが、 案外長くなったので個別に。 今日は field キーワードの話になります。 (昔のブログを参照して「やっと入ったよ」だけ書いて終わりかと思ったら案外新規に書くことがあり。)

field キーワード

プロパティ内において field をキーワード扱いして、 「プロパティのバッキング フィールドを表す変数」にしようという案があって、 今は機能名としても「field キーワード」と呼ばれています。

class A(int x)
{
    // (既存の)自動プロパティ。
    public int X1 { get; set; }

    // X1 と同じ意味になる「field キーワード持ち」のプロパティ。
    public int X2
    {
        get => field;
        set => field = value;
    }

    // 片方を自動、片方を field 持ちにもできる。
    public int X3
    {
        get; // 自動。
        set => field = value; // field 持ち。
    }

    // 自動プロパティでできたことは一通りこっちでもできる。
    // (イニシャライザーも持てたり、get-only とか init とかも。)
    // (というか、扱いは完全に自動プロパティと同じ。)
    public int X4 { get => field; } = x;

    // get 省略形の => 内でも field が使える。
    // int X5 { get; } と全く同じ。(コンストラクターで初期化可能。)
    public int X5 => field;
}

2年前にすでに「場合によっては C# 11 に入っていたかも」と言っていたものがようやく C# 14 で入ります。 当時は「半自動プロパティ」(semi-auto properties)とか呼んでいましたが、 結局「field キーワード」で行こうという感じになっているみたいです。

Visual Studio 17.12 Preview 3 / .NET 9 RC 2 の頃にはすでに merge されています。 つまり、C# 13 正式リリース(.NET 9)よりも前に、 すでに C# 14 の preview 機能が取り込まれている状態。 結構長いこと検討していて実装もあるものの、いくつか懸念があって延びに延びていて、 ようやく preview として世に出すことに。

懸念の1つは、これが「そこそこありえる」頻度の破壊的変更になることです。 「field という名前のフィールドがあって、this. は付けずに、プロパティの中で参照している」という状況が破壊的変更になります。 (「そこまで多くはないけど、まあそういう人も一定数いる」レベル。)

class A
{
    int field;

    public int Field
    {
        // C# 13 まで: field フィールドの参照。
        // C# 14 から: field キーワード。
        //             field フィールドはノータッチになる。
        //             field フィールドを参照したければ @field とか this.field にする。
        get => field;
        set => field = value;
    }
}

このコードは一応、C# 14 では警告になる予定です。 「field キーワードが field フィールドを隠してるけども意図通りか?」と怒られて、@field への書き換えを推奨されます。 もしかすると、今年のうち(C# 13 の間)に、「今のうちから @field に書き換えておいてくれ」アナライザーが提供されるかもしれません。

ちなみに、プロパティ内において、field は完全にキーワードになっています。 当初は「既存のコードを壊さない限りにキーワード扱いする」みたいな努力をするかどうかという話もあったんですが、複雑すぎるので断念しています。 例えば、field という名前のローカル変数があったとしてもキーワード扱いです。

class A
{
    public int X
    {
        get
        {
            var field = 1;
            return field; // これは field キーワード。フィールドの場合と同じく警告あり。
        }
    }
}

nameof(field) もエラーになります。 nameof(int) とかがエラーなのと同じ。

class A
{
    public string X
    {
        get => nameof(field); // ダメ。
    }
}

(余談で、value もキーワードに変えちゃうかという話もあったんですが、これは没になりました。)

これと関連して、以下のようなコードを書くと、タプル要素名のやつだけエラーを起こします。

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

            return field;
        }
    }

    class Foo
    {
        public int field;
    }
}

最後にもう1つ、null 許容参照型のフロー解析の問題があります。 プロパティが T のとき、そのバッキング フィールド(field キーワードの実体)は T であるべきか、T? であるべきか。

例えば以下のような ??= を使った遅延初期化コードはよく書くと思います。

class A(Type type)
{
    // Type.Name のキャッシュ。
    public string Name
    {
        // 遅延初期化にしたいので field ??= で代入。
        get => field ??= type.Name;
    }
}

現状(Visual Studio 17.13.0 Preview 2.1 時点)、「プロパティが T なら fieldT」です。 この例の場合、string (not null)。 「not null なフィールドがあるのに、コンストラクターで初期化していない」という警告が出ます。

解決策は検討さいれているんですが、短期的には MaybeNull 属性を使って回避してくれと言われています。

using System.Diagnostics.CodeAnalysis;

class A(Type type)
{
    [field: MaybeNull] // この属性によって、field が string? 扱いになる。
    public string Name
    {
        get => field ??= type.Name;
    }
}

上記解決策が間に合うなら、 「いったん fieldT? と仮定してフロー解析して nullable 警告を起こすかどうか」をみてバッキング フィールドが TT? かを決定するとのこと。 これが入れば MaybeNull を付ける前のコードでも警告が出なくなる予定です。

First-class な Span 型

$
0
0

「Rosly の Language Feature Status に並んでいるもののうち、すでに preview 提供済みのものシリーズ第2段。

  • field キーワード
  • First-class Span ← 今日はこれ
  • nameof(T<>)

すでに今、LangVersionpreview を指定すれば利用可能です。

今日は First-class Span。 (これも昔1回取り上げてるんですが、案外書くことあり。)

First-class Span

C# 7.2 の頃に Span<T>ReadOnlySpan<T> が導入されて以来、 これらの型を使った高パフォーマンスな API がたくさん提供されています。 また、C# 12 で入ったコレクション式や、 C# 13 で入った params コレクションでは、 T[]IEnumerable<T> よりも Span<T>ReadOnlySpan<T> を優先的に選ぶように特別な処理が入っています。 この例からもわかるように、今や Span<T>ReadOnlySpan<T> が重要な地位を占めています。

ところが、コレクション式などの一部の文脈を除いて、 Span<T>ReadOnlySpan<T> は「ただの構造体」で、 配列からの型変換も「Span<T>ReadOnlySpan<T> 構造体に定義されたユーザー定義型変換」です。 C# 言語組み込みの型変換と比べて、ユーザー定義型変換は1段下扱いで、色々な不便があります。

そこで、Span<T>ReadOnlySpan<T> を言語組み込み(= first-class、一級市民)にしたいという提案があって、 これもすでに実装があり、 Visual Studio 17.13.0 Preview 1 (.NET 9 の正式リリースと同時)で merge 済みです。

わかりやすいのは拡張メソッドの呼び出しで、 ユーザー定義型変換を挟む拡張メソッド呼び出しはできません。 例えば、以下のコードは C# 13 でコンパイル エラーだったものが、preview ではコンパイルできるようになっています。

// 拡張メソッドの呼び出しはユーザー定義の型変換を見ない。
// Span の特別扱いがないと拡張メソッドは呼べない。
new int[1].M();

static class Ex
{
    public static void M<T>(this Span<T> _) { }
}

また、 「Span<T>ReadOnlySpan<T> 引数を使った方がパフォーマンスがいいのでこちらを呼んでほしい」という要望があるんですが、 これまでは IEnumerable<T> なオーバーロードがあるとそっちが優先されるという問題もありました。

// ユーザー定義の型変換よりも、「派生・実装クラスだから変換可能」の方が優先度が高い。
Ex.M(new int[1]);

static class Ex
{
    public static void M<T>(this IEnumerable<T> _) { } // C# 13 ではこっち。
    public static void M<T>(this ReadOnlySpan<T> _) { } // preview ではこっち。Span の特別扱いがないとこっちは呼んでもらえない。
}

また、ユーザー定義の型変換では「型引数の共変性」を表現できないという問題があります。 ReadOnlySpan<string>ReadOnlySpan<object> に代入できてもいいはずなのに、 これが C# 13 まではできませんでしたが、preview にすると受け付けます。

ReadOnlySpan<string> s = [];
ReadOnlySpan<object> span = s; // C# 13 ではエラー。

ちなみに、Span<T>ReadOnlySpan<T> の両方のオーバーロードがある場合、 ReadOnlySpan<T> の方が優先されます。

string[] s = [];

// ReadOnlySpan の方が優先。
s.M();

static class Ex
{
    public static void M<T>(this Span<T> _) { }
    public static void M<T>(this ReadOnlySpan<T> _) { }
}

target-typed で生成される型自体が変わるコレクション式と違って、 一度配列を作っちゃってるので ReadOnlySpan<T> を優先しても別にパフォーマンス的なメリットは少ないんですけども。 じゃあどうしてこういう仕様にしたかというと… こうしておかないとまた「配列の共変性の地雷を踏むから」とのこと。

string[] s = [];
object[] o = s; // C# の配列は共変。

// Span を優先するとこれが例外を起こしちゃう。
// ReadOnlySpan<object> x = s; は合法。
// Span<object> x = s; は実行時例外。
o.M(); // ReadOnlySpan<object> を優先しないとここで例外が出る。

static class Ex
{
    public static void M(this Span<object> _) { }
    public static void M(this ReadOnlySpan<object> _) { }
}

以上、とりあえず、C# 14 (現状 LangVersion preview)では Span<T>ReadOnlySpan<T> が特別扱いされて、オーバーロードの解決順位が変わります。 おおむね便利な方向に変わるはずですが、もしかすると何らかの問題を起こす可能性もあります。 もしも「Span<T> オーバーロードを呼ばれるとまずい」みたいなことがあれば、OverloadResolutionPriorityとかでの対処を考えてみてください。

Viewing all 483 articles
Browse latest View live