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

C# の null 判定の話

$
0
0

null、一般名詞としては「無効なもの」とか「0個」とかの意味の単語です。 zero も語源をたどるとアラビア語とかサンスクリット語の「空っぽ (nothing)」にあたる単語から来ていて、実のところ一般名詞としては出自が違うだけで null = zero だったり。

一方、C# (とそれに類するプログラミング言語)では、 null というキーワードを「無効なものを 0 を使って表す」という意味で使っていて、 一般名詞としての null が持つ2つの意味を同時に指していたりします。

とはいえ、別に null という英単語の意味を考慮して「無効なものを 0 を使って表す」にしたわけではなくて、 単に実装上「0 かどうかの判定は非常に高速なのでパフォーマンス的に都合がいい」という現実的な理由で 0 を使っています。

前置きが長くなりましたが、C# において null 判定をするというのは、内部的には単に 0 比較で、 大体の CPU 上で最速の部類に入る命令を使って実装できます。

x == null

null 判定というとまずどういうコードを思い浮かべるでしょうか? 「昔から書けた」という意味で、まず x == null が真っ先に思い浮かぶ人が多いと思います。

bool M(A x) => x == null;
class A { }

これも、この状態であれば単なる 0 比較になります。 実際、コンパイル結果を覗いてみればわかるんですが、以下のコードと同じコードが生成されます。

bool M(int x) => x == 0;

ただ、ここで問題になるのが演算子オーバーロードでして、これをやっちゃってると「単なる 0 比較」ではなくなってしまいます。 特に以下のように、== の中でそこそこ重たい処理をやっちゃっているときが問題になります。

bool M(A x) => x == null;
class A
{
    public static bool operator ==(A x, A y) => そこそこ重たい処理;
    public static bool operator !=(A x, A y) => そこそこ重たい処理;
}

== を使っている側、この例でいうと メソッド M の中身は最初にあげた「速い」コードと同じ見た目なのが罠で、「本当は 0 比較でいいはずなのにわざわざ重たい operator == が呼ばれてしまう」という状況が往々にして発生します。

過激派な意見としては「== をオーバーロード(ユーザー裁量で中身を変更)可能にしてしまったことがよくなかった」という話もあるんですが、まあ、できるものは仕方がないとして。 本来の「無効かどうかの判定は単なる 0 比較で済む」という状態にしたければ、== を避けた方がいいということが多々あります。

ReferenceEquals(x, null)

この罠にはまっちゃってるコードは案外世の中にあふれているというか、 .NET の標準ライブラリでも結構あったみたいです。

この問題は昔の C# でも簡単に解消する方法が1つあって、それが、ReferenceEquals を使うという案。

bool M(object x) => ReferenceEquals(x, null);

これで、ユーザー定義の == オーバーロードは呼ばれることなく、常に 0 比較で null 判定が走ります。

めでたしめでたし。

となるわけはなく、見栄えが悪すぎる…

ということで、「ReferenceEquals に書き換えて問題ないし、書き換えたら露骨にパフォーマンスがよくなるんだけど、この見栄えの悪さを許容するべき?」みたいな議題になっていました。

x is null

そこに来て、C# 7.0 でパターン マッチングという文法が入りました。 この頃には「== null の罠」が周知の事実だったので、「is null と書いたときにはユーザー定義の == を呼ばない。常に 0 比較にする」という判断が下りました。

bool M(object x) => x is null; // operator == は呼ばない。常に ReferenceEquals(x, null) と同じ。
class A
{
    public static bool operator ==(A x, A y) => そこそこ重たい処理;
    public static bool operator !=(A x, A y) => そこそこ重たい処理;
}

これに「見栄え的に ReferenceEquals は NG」派が飛びつきました。 == null から is null への書き換えで救われたコードが結構あったみたいです。 (実際、僕が保守しているコードでもいくつかこの書き換えでパフォーマンス改善しています。)

めでたしめでたし?

非 null

めでたくなかった。

実際に多いのは以下のようなコードだったりします。

void M(A a)
{
    var x = a.X; // プロパティ参照コストを避けるために変数に受ける。
 
    if (x is null) return;
 
    // x を使って何か処理をする。
}

class A
{
    // virtual がついていたり、いくつかの場面では X プロパティの参照に多少コストがかかる。
    public virtual object? X { get; set; }
}

これはいわゆる early return (先頭で検査して不適切なら即 return)な書き方ですが、 判定を逆転させて同じ結果になるコードを以下のように書きたいこともあります。

void M(A a)
{
    var x = a.X;
 
    if (!(x is null))
    {
        // x を使って何か処理をする。
    }
}

何にしてもポイントが2つあって、

  • 1度変数 x で受けたい
  • 「null である」判定じゃなく、「null じゃない」判定をしたい

という要件があって、x is null の導入だけだとまだちょっと面倒が残っている感じになっています。

x is object (非 null)

「null じゃない」判定に使える書き方はいくつかあるんですが、前半で話した x == null の話と同様、x != null はユーザー定義の演算子オーバーロードを呼ばれて遅くなることがあります。 そこで x is null と同様、比較的新しめの文法であるパターン マッチングを使った「null じゃない」判定が欲しくなります。

C# の場合、「null は型を持っていない」という扱いになるので、すべての型の共通基底クラスである object 型にすらマッチしません。 なので、以下のように、is object というパターンを書くと「null じゃない」という判定になります。

void M(A a)
{
    if (a.X is object x)
    {
        // ここに来るのは a.X が null じゃなかった時だけ。
        // x を使って何か処理をする。
    }
}

注意: x is var (null 判定しない)

ここで注意すべきことが1点。結構な罠なんですが、上記のように is object が「null じゃない」判定になるのに対して、is var だと null / 非 null に関わらず常にマッチします(is var 単体だと常に true)。

void M(A a)
{
    if (a.X is var x)
    {
        // ここは常に通る。
        // if なしで var x = a.X; と書くのとほぼ同じ意味なので非推奨。
    }
}

var パターンは switch-casedefault 句みたいなもので、「他のどの条件も満たさないときの最後の受け口」みたいに使うものです。 なので、今回の主題の null 判定に限らず、if 単体で使うものではありません。

x is { } (非 null)

もう1個、is { } という書き方でも「null じゃない」判定ができます。 知らないと何が何だかわからない謎な書き方ですが、 文法的にいうとこれは「プロパティ パターン」というものになります。

void M(A a)
{
    if (a.X is { } x)
    {
        // ここに来るのは a.X が null じゃなかった時だけ。
        // 起こる結果は is object x と同じ。
        // x を使って何か処理をする。
    }
}

本来は以下のように、再帰的にプロパティの中身を確認できる「パターン」です。

void M(A a)
{
    if (a is { X: object x })
    {
        // a の中身の X プロパティの中身をチェック。
        // ちなみにこの場合、a 自体の null チェックもかかるので、a != null && a.X != null と似た処理。
    }
}

ただ、{} の中に何もなくても「null じゃない」判定だけはかかるので、その用途に流用できます。 ちょっと濫用・悪用気味ではありますが、「null じゃない」判定をしつつ変数で受ける手段としては一番短い書き方になります。

is not null

x is { } は最も短く「null じゃない」判定を書ける手段ではあるんですが、 なにぶん濫用気味な書き方で、知らない人が見て理解しにくい、知っていても「null じゃない」という意図が伝わりにくいという問題があります。

そこで結局、!(x is null) という書き方の方がいいんじゃないかという話にもなるんですが… これはこれで、!() も十分に見にくい(() が邪魔だし、意味を真逆にする割には ! という記号は視認性が悪すぎて見逃す)という問題があります。

あと、以下のような「書き間違い」をする人が後を絶たないという問題も起こしました。

void M(A a)
{
    if (a.X !is null) // is not のつもりで !is とか書く
    {
        // ちなみにこの ! は not の意味にならず、このコードは is null (意図と真逆)になる。
    }
}

この !null 判定の抑止、要するに、コンパイラーが正しくフロー解析できなさそうな微妙なコードで、コンパイラーの警告をもみ消すために使う演算子です。 フロー解析(あくまでコンパイラー内での処理)に使うだけであって、この ! の有無はコンパイル結果には全く影響を及ぼしません。 なので、x !is nullx is null が全く同じ意味。

一方、C# 9.0 では not パターンというものが導入されて、今度こそ is not の意味のパターンが書けるようになりました。

void M(A a)
{
    if (a.X is not null)
    {
        // ちゃんと null じゃないときだけここを通る。
        // != null と違ってユーザー定義演算子は呼ばれず、単なる 0 比較。
    }
}

is not { } (null の時に early return)

ここからは C# 9.0 のバグの話。 Visual Studio 16.8 (C# 9.0 の初期リリース。2020年11月リリース版)時点の C# には is not { } x という書き方にバグがあります(is not object x でも同様にバグあり)。

not { } は「null じゃない」をさらに否定しているので結局「null である」という判定になります。 単に「null である」判定をしたいだけなら is null と書けばいい話なんですが、 「変数で受けつつ null である判定」という処理をしたいときに is not { } x という書き方をします。

void M(A a)
{
    if (a.X is not { } x) return; // null だったら early return。
 
    // x を使って何か処理をする。
    // ここでは x に非 null な値が入っているはず。
}

is not { } xis not object x とい書き方はまさにこの「null のときに early return」のためにあって、null じゃなければその値が変数 x に入った上で else 側に流れます。

ですが、バグで、時々その「null じゃない値を変数 x で受ける」という処理が消えてしまうことがあるそうです。 上記コードはちゃんと動くんですが、例えば以下のコードだと x が null のままになっていて実行時例外を起こします。

using System;
 
M("abc");
 
void M(string? s)
{
    if (s is not { } x) return; // null だったら early return。
 
    // x には s が代入されていないとおかしいはずなのに…
    Console.WriteLine(x.Length); // ここでぬるぽ(バグ)。
}

バグです。 バグ報告済みというか、報告されて早々に修正・ merge 済みで、Visual Studio 16.9 では直る見込みです(16.8.1 とかにもこの修正が取り込まれるかは未定)。


Visual Studio 16.9 Preview 3 (C# Next チョットある)

$
0
0

Visual Studio 16.9 Preview 3 が出たということでライブ配信をしていました。

冒頭で話しているんですが、Preview 1 は 16.8 正式リリースと同時だったので 16.8 の方を紹介、 Preview 2 はそんなに大きな変化もなかったので、Preview 3 で初めて 16.9 の話です。

ポロリ

YouTube 配信前に作ってあるお品書き issueのタイトルには「C# Next ポロリもあるよ」とか書いているんですが。 まあ変なタイトルを付けたかっただけで、ポロリといっても「出ちゃいけないものが出ちゃった」みたいなことはないです。

  • C# にもちょっとした新 preview 機能追加あったよ
  • 9.0 には間に合わなかったものがさらっと merge されたよ

くらいの意味でポロリと言っています。

昨年、C# 9.0 のときは最初に Preview 機能追加があったのは4月のことだったので、小さな機能追加といってもずいぶんフットワークが軽くなったなぁと思います。 ほんとにもうできたものから Preview リリースしていくんだ、と。

LangVersion Preview

9:28~

復活の LangVersion Preview。

今年はほんとに <LangVersion>latest</LangVersion> (とか default) とは短いおつきあい(2か月ほど)でした。

Constant Interpolated Strings

10:18~

入ったのは Constant Interpolated Strings(文字列補間の const 扱い)です。

class Sample
{
    public int A { get; }
    public int B { get; }
    public const string S = $"{nameof(A)} {nameof(B)}";
}

文字列補間の $"{}"{} の中が全部定数の時、$"{}" も定数扱いされるという仕様になりました。

10:39~

ちなみに、{} の中には、たとえ const であっても数値とかは受け付けず、const string の入れ子だけを受け付けます。

public const string S = $"{123}";

23:00~

なんでかというと、文字列以外の文字列補間は ToString を経由していて、その ToString はカルチャー依存だから… コンパイル時に定数化できません。

カルチャー依存の例:

using System;
using System.Globalization;
 
var ja = CultureInfo.GetCultureInfo("ja-jp");
var fr = CultureInfo.GetCultureInfo("fr-fr");
//Console.WriteLine(double.Parse(1.234.ToString(ja), fr)); // 例外
Console.WriteLine(double.Parse(1.234.ToString(fr), ja)); // 1234 扱い…
 
Console.WriteLine(double.Parse(1.234.ToString(fr))); // CurrentCulture...
 
System.Threading.Thread.CurrentThread.CurrentCulture = fr;
Console.WriteLine(double.Parse(1.234.ToString(fr))); // CurrentCulture...
1234
1234
1,234

IDE Productivity

33:56~

#if にコード補完がかかるように。

1:13:46~

警告等のツールチップヒントからヘルプページに飛べるように。

1:36:53~

Source Generator の生成物のファイルが Solution Explorer 上に表示されるように。

1:39:2~

同じく Source Generator の生成物のファイル、Ctrl + , とかの検索結果に現れるように。

2:47:00~

XAML からの ViewModel C# コード生成ができるように。

OR_GREATER

44:23~

OR_GREATER シンボル…

OR_GREATER に関するアナウンスは特に大々的に行われておらず、 #if のコード補完の話のスクショでしれっとお披露目…

「あるバージョンより新しいものだけで」みたいな条件コンパイルをしたいという話は昔からありまして。 それがついに入りました。

当初予定では「NET5_0 だけで .NET 5 以降」という扱いで行くつもりだったものの、 .NET 5 リリース直前でダメだしを受けて急遽取りやめ。 その代案として出て来た話がこの「_OR_GREATER を付ける」でした。

52:32~

NETSTANDARD には OR_GREATER シンボルはないとか…

56:38~

NET4 系(要するに .NET Framework) と NET5 系(".NET" に統合後)は不連続なので注意とか…

59:00~

大人は嘘つきではないのです。間違いをするだけなのです。

CsWin32

1:43:10~

ちょっと本題(VS 16.9 Preview 3、C# 10.0)から離れましたが、Win32 P/Invoke の話題でも盛り上がったり…

ちょうどこの配信の日に win32metadata とか CsWin32 とかの告知が出ていて、「Twitter とかでみんな盛り上がってるなぁ」とか思っていたら、自分のライブ配信でも盛り上がるなど。

Win32 API の P/Invoke 用のコードを Source Generator で生成してくれるものです。

2:34:41

Linux マシン上とかでもちゃんとソースコード生成からビルドまでできてちょっと盛り上がりました。 (さすがに実行はできなくて、DllNotFound。)

2:02:42~

C# の string とかからネイティブの char* とかへの変換などのいわゆるマーシャリング処理は今まで .NET Runtime 内で動的にやっていたみたいなんですが、この度 Source Generator 化されたことでパフォーマンス的にもちょっとうれしいはずです。 これまでも .NET Native とかではビルド時にやっていたそうなので、それが .NET Native (UWP 向けの AOT)という特定環境向けのビルド ツールに限らない汎用的なものになりました。

UAX31: Unicode Identifier の話

$
0
0

今日はまた去年の作業が元ネタで、プログラミング言語の識別子に使える文字に関する話です。

レターか数字

「1文字目にはアルファベットか _、2文字目以降にはそれに加えて数字を使えます。」

30年くらい前にはこれが「プログラミング言語の識別子(変数名など)に使える文字列」の定義でした。 _ の部分はプログラミング言語次第ですが、「1文字目にアルファベット、2文字目以降に数字」の部分は結構いろんな言語でそうだったんじゃないかと思います。

まあ、昔のプログラミング言語は ASCII コードで書く物だったので、上記の条件は [a-zA-Z] とか [0-9] みたいな正規表現で書けたんですが。 Unicode の時代になると「アルファベットだけでいいのか」とか「アルファベットって何だ」という話になります。

レター

まず、「アルファベット(alphabet)」というと母音と子音が分かれてる文字のことで、ラテン文字、ギリシャ文字、キリル文字なんかのことを指します。アラビア文字みたいに母音しか表記しないやつはアブジャド(abjad)、漢字は表意文字(ideogram)、ひらがな・カタカナみたいなのは音節文字(syllabary)と呼ぶらしく、「1文字目はアルファベット」と言ってしまうと一部の自然言語に偏ってしまいます。

Unicode 的にこの辺りの「記号や数字じゃない文字全般」を指してレター(letter)と呼ぶので、冒頭の条件は以下のように書き換わります。

「1文字目にはレターか _、2文字目以降にはそれに加えて数字を使えます。」

レターとは…

Unicode では文字ごとにカテゴリーが決められているので「ある文字列がレターかどうか」を調べるのは簡単…

かというとそうでもなくて、確かに1文字1文字がレターかどうかを判定するのは素直なんですが、2文字以上がくっついて1文字になることがあってそれが面倒だったりします。

例えば「ら゚」の文字。 現代日本語では普通は使わない文字ですが、R 音の「ら」と L 音の「ら」を区別するために L 音の方を「ら゚」と書く用法が一時期あったそうです。今現在ほとんど流通していないレア文字なので、この文字を Unicode 1文字で表す方法はない(符号が割当たってない)んですが、普通の「ら」(U+3089)の後ろに半濁点(U+309A)を並べることで「ら゚」を表せます。

ちなみに、Unicode の文字カテゴリー的には

  • ら(U+3089) は Letter, Other (Lo)
  • ゚゚ (U+309A) は Mark, NonSpacing (Mn)

となっています。マーク(mark)ってものが出てきましたが、「レターにくっついて修飾する系の文字」は大体この分類です。日本語の濁点・半濁点以外にも、ラテン文字に対するダイアクリティカルマークもマークの類です。

日本語とかラテン文字の場合はこういうレア文字を除いてほとんどの文字が、 わざわざレター + マークの組み合わせを使わなくても大体 Unicode の符号が割当たっているのでそこまで困りません。 一方で、タイ文字(อักษรไทย みたいなの)とかサンスクリット(संस्कृत みたいなの)は普通に日常的に使う文字がレター + マーク構成になっています。

ということで、「人の認識上」でレターっぽいものは受け付けたいとなったとき、 「Unicode の処理の都合上」では「1文字目にレター、2文字目以降にマーク」になります。 その結果、冒頭の条件はさらに以下のように書き換わります。

「1文字目にはレターか _、2文字目以降にはそれに加えて数字とマークを使えます。」

Unicode カテゴリーを使ったちゃんとした定義

この「1文字目にはレター、2文字目以降にはそれに加えて数字とマーク」という方向性、たぶん最初に採用したのは Java ですかね。isJavaIdentifierStartisJavaIdentifierPart というメソッドで判定してるみたいなんですが、ここに並んでいる条件がおおむね「レターと数字とマーク」です。

C# の場合は Unicode カテゴリーがそのまま列挙されていて、

  • 1文字目: Lu, Ll, Lt, Lm, Lo, Nl
  • 2文字目以降: 上記に加えて、Mc, Nd, Pc, Cf

みたいになっていますが、これがだいたい「レターと数字とマーク」になります。

カテゴリーの安定性問題

Unicode 曰く、「カテゴリーはできる限り安定させたいけど希に変わることがある」とのこと。

実際、日本語だと以下の文字のカテゴリーに変更がありました。

  • ゛ と ゜ (U+309B と U+309C、単独の濁点・半濁点)
    • これとは別に、マーク(直前のレターにくっつく)扱いの濁点・半濁点がある(U+3099 と U+309A)
    • 昔はマークの方の濁点・半濁点との混同があった
    • 今は Symbol, Modifier で識別子として使えない
  • ・ (U+30FB、中黒)
    • 昔は Punctuation, Connector で識別子の2文字目以降に使えた
    • 今は Punctuation, Other で識別子として使えない

特に・(U+30FB) の変更は比較的新しい話で、 Java 7 (2011年)とか C# 6.0 (2015年) の頃に「今までコンパイルできていたコードが急にコンパイルできなくなった」みたいな騒ぎがありました。

UAX31

Unicode、「何番のコードに何の文字を割り当てるか」みたいな基本的な定義に加えて、例えば以下のような様々なレポートを出していたりします。

昔は Unicode Technical Report、今は Unicode Standard Annex (付録)みたいに呼んでいるようで、 後者は UAX と略したりします。

で、その中に「識別子として使える文字」の話もあります。通称 UAX31。

あくまで recommended defaults (推奨される既定動作)であって何か拘束力のある標準仕様ではないんですが、 「迷うくらいならこれに従っておけ」くらいの材料にはなります。

概ね Java/C# のものを踏襲していそうな感じで、以下の条件がベースです。

  • 1文字目: Lu, Ll, Lt, Lm, Lo, Nl
  • 2文字目以降: 上記に加えて、Mc, Nd, Pc, Cf

少なくとも2003年のバージョン1はほぼこの条件。 違いというか、先ほどのカテゴリーの安定性問題を避けるためにいくつか付帯説明があります。

  • Alternative Identifier」(代替案)として、一部の記号だけ避けてほぼすべての文字を識別子として使える案もある
  • 後方互換性のため、4文字ほど、カテゴリー変更があった文字に追加で Other_ID_Start という属性を持たせて識別子として使えるようにしている

2020年のバージョンではもう少し複雑になっていますが、大体安定性のためです。 Other_ID_Start だけでは文字のカテゴリー変更に対応できなかったみたいで、追加で Other_ID_Continue という属性が定義されています。 また、Alternative Identifier 向けに定義している Pattern_Syntax (プログラミング言語の構文に使いそうな記号類)、Pattern_White_Space (同、空白文字の類)を避けることが明言されています。 これら Other_ID_Start 、Other_ID_Continue、Pattern_Syntax、Pattern_White_Space は、カテゴリーと違って、今後破壊的変更を起こさないように運用するとのこと。

また、初期バージョンで「Alternative Identifier」と呼んでいたものは、現在は「Immutable Identifier」という呼び名に変わっています。 名前通り、Unicode のバージョンによらず常に不変な保証があります。 ただ、何の文字でも受け付けすぎるのであまり推奨はされていません。

C++ の UAX31 採用

Java と C# は、Unicode のカテゴリー変更を受け入れる方向性になっています。 中黒(U+30FB)のカテゴリー変更のとき、そこまで大きな問題にしなかったので。 UAX31 のオプションとして使ってもいい文字のテーブルに中黒(KATAKANA MIDDLE DOT)が追加されたりはしましたが、 それだけです。

これに対して、C++ (かつては ASCII 文字しか受け付けなかった)が Unicode 識別子に対応しようとした際には UAX31 に従おうという話になったそうです。

UAX31 Immutable Identifier

ただ、問題は UAX31 の安定性。 ちょうど Java が中黒(U+30FB)問題を踏んだ時期だったので、カテゴリー変更を警戒して、 Alternative Identifier (現在の Immutable Identifier)を採用しようとしました (2010年発案)。 (結局標準化はしてなさそう?ですが)いくつかの C++ コンパイラーはこの案で実装あり。 例えばClang は 3.3 でgcc は 10 で対応。

と言うことで現在、たいていの C++ コンパイラーで以下のコードがコンパイルできます。

#include <iostream>
 
int main()
{
    int 😱 = 2;
    int 😇 = 3;
    int 🥺 = 5;
    std::cout << 😱 * 😇 * 🥺 << std::endl;
}

UAX31 Default Identifier

その後、前述のとおり UAX31 にも手が入っていて、安定性が改善しました。

と言うことで改めて、C++ の標準仕様として、C++ の識別子を UAX31 に従うようにしようという話になっているみたいです。 今度はちゃんと Default Identifier (Java とか C# とかに近いやつ)で。

現状、賛成多数で、C++ 23 で採用されそうな雰囲気。

ということで、Immutable Identifier (絵文字を含んでる)から Default Identifier (絵文字を含んでいない)に変更されそうです。前節の絵文字ソースコードは C++ 23 からコンパイルできなくなる予定。

Immutable Identifier から Default Identifier への変更

これまで使えてた文字が使えなくなる(破壊的変更する)わけで、 この提案にはそれ相応の説得材料が必要になります。 なので、P1949R6 には結構詳細に、これまで(Immutable Identifier)の問題とか、変更の影響がまとめられています。

その中から2つほど紹介。

  • 絵文字が使えなくなる問題
  • 元から一部の絵文字は使えなかった問題

絵文字が使えなくなる問題

まんま抜粋。

class 💩 : public std::exception { };

Throwing “PILE OF POO” becomes ill-formed. Conference slide-ware will be less entertaining.

(絵文字が使えなくなることで) うんこ投げるコードは受け付けなくなった。スライド映えしなくなる。

基本的に破壊的変更をよしとしない C++ で、破壊的変更が賛成多数になるくらいですから…

絵文字識別子の扱い、やっぱり「スライド映え」ですよね。

元から一部の絵文字は使えなかった問題

Immutable Identifier は

  • 所定の文字を1つ1つリストアップして識別子として使えないように禁止してる
    • 基本的には記号類は禁止
  • サロゲートペアは無条件に許可

みたいなことをしているので… 以下のように、使える絵文字と使えない絵文字があります(not valid コメントの行のものだけダメ)。

int ⏰ = 0; //not valid
int 🕐 = 0;
 
int ☠ = 0; //not valid
int 💀 = 0;
 
int ✋ = 0; //not valid
int 👊 = 0;
 
int ✈ = 0; //not valid
int 🚀 = 0;
 
int ☹ = 0; //not valid
int 😀 = 0;

要するに、「基本的に記号を禁止しているのに、サロゲートペアなやつは禁止されない」という状態。 上から順に文字コードは以下のようになっています。

  • ⏰ : U+23F0
  • 🕐 : U+1F550
  • ☠ : U+2620
  • 💀 : U+1F480
  • ✋ : U+270B
  • 👊 : U+1F44A
  • ✈ : U+2708
  • 🚀 : U+1F680
  • ☹ : U+2639
  • 😀 : U+1F600

この辺りはまあ、「なんか変だな」で済む話なんですが、1個、ポリコレ的な地雷を踏みそうな事案も発見されています。

bool 👷 = true; //  Construction Worker
bool 👷‍♀ = false; // Woman Construction Worker ({Construction Worker}{ZWJ}{Female Sign})

男の建築作業員はよくて女の建築作業員はダメなのか!

これ、Emoji ZWJ Sequence というやつでして。 「絵文字が特定の性別に偏っている」という問題に対する解決策として、「絵文字をいくつか ZWJ でつなぐことで別の字形に変える」という対処をしています。 で、👷‍♀ の絵文字シーケンスが Female Sign (♀、U+2640)を含んでいて、これが「サロゲートペアじゃない記号」なので Immutable Identifier でも禁止されている文字になります。

当初は「スライド映えしなくなるくらい別にいいよね」というだけの問題だったものが「ポリコレ的に今のままの方がまずい」になったことでちょっと賛成票が増えたみたいです。

まとめ

元々は C# の識別子について調べてる過程で、源流は Java っぽいという話だったんですが、 近い仕様が Unicode の推奨仕様(UAX31)になっていました。 ここまでは結構前から知っていたものの、UAX31 に類する仕様を採用しようとするプログラミング言語は少数派だと思っていました。

それが最近になって、C++ が実質的に破壊的変更になりえる状況で UAX31 Default Identifier を採用しそうな流れになっていて、提案文書に「💩投げれなくなる」のパワーワードが含まれていたという話でした。

ピックアップRoslyn: raw string literal

$
0
0

ブログで取り上げたい C# Language Design Meeting 議事録が1か月分くらいたまっているわけですが:

1/272/32/82/102/22

しばらく、機能ごとに1個1個取り上げていこうかなという感じになっていまして、今日は raw string literal の話から。

概要

以下のような書き方で、複数行、かつ、一切のエスケープなしの文字列リテラルを導入したいという話が出ています。

string xml = """
    <a>
        <b c="abc" />
    </a>
    """;
 
string json = """
    {
        "a" : {
            "b" : {
                "c" : "abc"
            }
        }
    }""";

C# の文字列中に XML、Json/JavaScript とか、さらに言うと C# 自身を書きたいことが多々あって、 結構昔から要望はありました。

近い機能として、C# には 1.0 の頃から @"" という書き方があったりしますし、Visual Studio などの IDE が自動補完で補ってくれたりはしていました。

string s = @"
{
    ""a"" : ""abc""
}";

Visual Studio の自動補完で複数行文字列を書く例

でも、 \" とか {{ とか "" とか書くのがめんどくさいのでもっと「生文字列」を書きたいということで出て来た文法案になります。

背景: 言語内言語

C# 中に XML とか Json とか SQL を直接リテラルに埋め込もうとして、 外部で編集して来たものをコピペしたら " のエスケープで困るということはよくあると思います。

C# の文字列中に何らかの言語を埋め込むこと自体どうなんだという話はあるんですが、 最近の C# は普通に言語内言語を解釈してるんですよね。

言語内言語

今のところ正規表現時刻・日付のフォーマットだけなんですが、一応、C# コンパイラー内には任意の文字列に対して自作の色付け(highlight)・補完候補(completion)を出すための仕組みを持っていたりします。

C# 9.0 世代では Source Generator も入ったわけで、 言語内言語を書く機会は増えるかもしれません。 というか、自分は割かし定期的にもくろんでいます

上記の正規表現・時刻・日付の実装は今のところ internal なんですが、 Source Generator によって需要が増えれば、言語内言語の色付け・補完用の機能も public にしてもらえるかもしれません。 (一度 public にすると変更が難しくなるので、結構しっかり作ってあっても需要が上がらないとなかなか public にならない。)

昔から要望がある raw string literal の具体案が今になって本腰が入ったのは、こういう背景からだと思います。 (ちなみに、raw string literal 提案のオーナーになっているのは上記の正規表現・時刻・日付の色付け・補完の実装者の方だったりします。)

3個以上、任意個の "

この手の raw string を実装する上で問題になるのは、「自分自身を含む可能性」だったりします。 C# Source Generator で C# から C# をコード生成したりするとき、結構困ります。

C# 1.0 の頃からある @"" の一番の問題点も " 自体を含みにくい("" と、2文字並べるエスケープ処理が必要)ことです。 なんせ、XML、Json など含め、たいていのものが文字列の表現に " を使います。 物によっては ' (single quotation) か " (double quotation) かを選べたりしますが、両方を含むことも多いです。 まして、Java とか JavaScript とか C# とかは C 言語由来の文法が多くて、" とか { とかの使い方がほとんど同じです。 これらをそのまま書けないというのが @"" の使いづらいところです。

そこでまあ、""" とか、「" を3つ並べた場合、そのあと、再び """ が現れるまで " をエスケープなしの生の " 扱いする」みたいな文法が考えられます。 ところが、「その文法を採用した自分自身」を含もうとすると、""" をエスケープできる手段が再び必要になったりします。

ということで、今提案されている raw string literal では「3個以上、任意個の " から開始して、同数の " で終わる」という仕様にしてあります。""" (3個)をエスケープなしで含みたければ """" (4個)から開始すればいいじゃない。

var cs = """"
    var s = """
        C# in C#
        """;
    """";

インデント

もう1個、@"" の嫌なところはインデントがそろわなくなるところです。

以下のような文字列リテラルを書くと、1行目の位置と2行目以降の位置がだいぶ離れるのが結構見づらくなります。

class Program
{
    static void M()
    {
        const string s = @"1行目
    2行目
3行目
";
    }
}

本当は以下のように書けるとだいぶ見やすくなると思います。

const string s = @"
    1行目
        2行目
    3行目
    ";

もちろんこれは有効な C# コードなんですが、1行目の前に改行文字が1つ入っちゃうのと、全部の行の行頭にスペースが入っちゃうわけで、空白文字を含めて一字一句一致しないとまずい場合には使えません。

ということで、今回の提案の raw string literal は以下のような仕様になります。

  • 開始の """ (3個以上の ") の後ろには改行必須
  • 1行目のインデントを基準にして、それよりも前の空白文字は無視

例えば先ほどの @"" で書いた文字列リテラルと同じものを新しい raw string literal で書くと以下のようになります。

const string s = """
    1行目
        2行目
    3行目
    """;

文字列補間はやらない

まあ一番の目的が「全くエスケープ処理をしないでいい文字列リテラルが欲しい」という点なので、$"" みたいな特殊なことは一切やらないそうです。

文字列補間には、$@"" もしくは @$"" という書き方で、「複数行、かつ、文字列補間が掛かるリテラル」が書けます。

    static void M(string @namespace, string className)
    {
        string s = $@"
namespace {@namespace}
{{
    class {className}
    {{
    }}
}}
";
    }

はい、もうこの時点で何が嫌かわかるかと思います。{{ が嫌。 実際、「C# 内 C#」みたいなことをやると、上記のコードは以下のように書き直した方がまだマシなんじゃないかと思ったりすることが結構あります。

    static void M(string @namespace, string className)
    {
        string s = @"
namespace " + @namespace + @"
{
    class " + className + @"
    {
    }
}
";
    }

ものすごく本末転倒…

ということで、とりあえず、raw string literal に関しては $""" みたいなものは提供しないとのこと。

他の言語の類似機能

似たような複数行・エスケープなし・インデント調整可能なリテラルを導入した言語もちらほらあります。

Java は Java 15 で text blocks という名前でこの機能を導入したみたいですが、開始文字は """ (3個)で固定みたいです。 自分自身を含むときにやっぱりエスケープが必要。

Swift は元々あった「""" (3個固定)で複数行文字列」(multiline string)という仕様に、 Swift 5 で導入した「任意個の # の後ろに " (単体行) か """ (複数行)でエスケープなし文字列」(raw string)みたいな文法を組み合わせて同様のことができるみたいです。先に """ (3個固定)があったから # になっちゃった感じ。

ピックアップRoslyn: 構造体の引数なしコンストラクター

$
0
0

前回の続き。 というかしばらく「C# Language Design Meeting 議事録が1か月分くらいたまったので1個1個機能紹介」シリーズ。

議事録(前回と比べて2/24議事録が増えてます):

1/272/32/82/102/222/24

今日は「構造体の引数なしコンストラクター」の話。

概要: 構造体の引数なしコンストラクター

現状ではコンパイル エラーになる以下のコードを書けるようにしようという話です。

struct S1
{
    public int X;
    public int Y;
 
    public S1() // これとか
    {
        X = 1;
        Y = 2;
    }
}
 
struct S2
{
    public int X = 1;
    public int Y = 2; // この2行とか
}

C# 6.0 の頃に一度採用しようとしたものの、Activator.CreateInstance のバグを踏んでしまって取りやめになっていました。 (6・7年前の話ですが、まあ、覚えている方も中にはいらっしゃるかも。)

上記議事録上は直接出てきてはいないんですが、record structs と関連して「やっぱり必要だよね」みたいな空気になっていて、改めて提案が出ました。

Language Feature Status の C# Next のところにも並んだので C# 10.0 に内定。

背景: default(T) と new T()

さかのぼること C# 1.0。 最初のバージョンの C# には defaultがありませんでした。 でも、(既定値の作成」(0 埋め)は必要で、それを構造体の場合、単に new T() と書いていました。

結果的に、C# の仕様は以下のようになりました。

  • 構造体には明示的に引数なしコンストラクターを書くことはできない
  • 構造体は暗黙的な引数なしコンストラクターがあるかのようなふるまいをしていた
    • 構造体に対して new T() と書くと既定値(すべてのフィールドを0埋め)を作る
    • : this() も0埋め処理になる

その後、C# 2.0 ではジェネリクスの導入に伴って default(T) という書き方で既定値を作れるようになりました。 この時点で実は、「構造体の new T() は既定値を作るために使う」という必要性はなくなっています。 ただ、あくまで「変えても大丈夫になった」というだけで、実際に変えようという話になったのは C# 6.0 が初出だし、実現しそうなのは 10.0 です。

つまり現状、

  • new T()default(T) は全く同じ意味
    • どちらも「規定値の作成」で0初期化

で、これを、 C# 10.0 で、

  • 構造体に明示的な引数なしコンストラクターを書けるようにする
  • 明示的な引数なしコンストラクターを書いた場合に限り、new T()default(T) が別の意味になる
    • new T() はコンストラクター呼び出し
    • default(T) は規定の作成

にしようとしています。

Activator バグ

C# 6.0 でこの話が出た時、なんで即座に実装できなかったというと、Activator.CreateInstance の実装に問題が発覚したからだそうです。

前述の通り、元々、構造体に対して new T()default(T) と同じ意味で使っていました。 Activator.CreateInstance はその前提で実装されていたそうで、 構造体に引数なしコンストラクターを書けるようにしたのにコンストラクターを呼んでもらえないという状態になったそうです。 当時、構造体に対する CreateInstance<T>() は常に既定値(default(T))を返す実装になっていました。 (ちなみに、 .NET の型システム上は元々構造体に引数なしコンストラクターを持たせられるにも関わらず、です。)

Activator.CreateInstance なんて使ったことないし、そんなに問題なの?」と思う方もいらっしゃるかと思いますが、 実際には多分、無意識に使っています。 と言うのも、ジェネリクスの new() 制約を付けた型を実際に new T() すると、内部的に Activator.CreateInstance<T>() が呼ばれます。

class C<T>
    where T : new()
{
    public static T M() => new T(); // これが実は Activator.CreateInstanct<T>() になってる。
}

C# 6.0 でこの問題に気づいたあとすぐに CreateInstance は修正されて、 今はちゃんと「構造体でも引数なしコンストラクターがある場合はそれを呼ぶ」という実装に変更されています。

この修正は .NET Framework 4.6 の時に入っていて、となると基本的に「サポートがまだ切れていない .NET ランタイムで CreateInctansce がバグっているものはもうほとんどない」という状態。 (時代的背景があって .NET Framework 3.5 SP 1 とかが実はまだサポート期間中だったりしますが、 さすがに .NET Framework 3.5 で C# 10.0 を使いたいという需要はほとんどないと思われます。)

そして実装へ (C# 10.0)

C# 9.0 で追加されたレコード型(まだちゃんと C# によるプログラミング入門内に書いてない…)は参照型(クラスと一緒)になります。 で、レコードと同じことを値型でもやりたいという話は 9.0 の頃から当然あって、 単に「後からの追加でも問題ないし、9.0 に間に合わないから 10.0 でやる」という話になっていました。 それが record struct。 基本的にはほぼ「C# 9.0 でレコード型に対してやったことをほぼそのまま構造体に対してもやる」というものです。

C# 9.0 のレコード型では、例えば以下のような書き方ができます。 型名の直後の () はプライマリ コンストラクターとか呼ばれています。

// プライマリ コンストラクター
record A(int x)
{
    public int X { get; init; } = x;
}
 
// 引数なしプライマリ コンストラクター
record B() : A(1)
{
    public int Y { get; init; } = 2;
}

これと同じようなことをしたいんだから、自然と、record struct でも以下のような書き方もできてほしくなります。

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

まあ、record struct は単なる契機であって、元から以下のような書き方をしたいという要望はずっと昔からあります。

struct S
{
    public int X = 1;
}

はい、いいタイミングなのでやりましょう(いまここ)。

踏みそうな問題

まあ概ね「今までできなかったことの方が不自然」レベルの機能なので、そんなに説明が必要な部分はないと思います。 「フィールド初期化子があると暗黙的に引数なしコンストラクターが作られる」とか「フィールド初期化子は上から順に呼ばれる」とか、大体は「クラスと一緒」の一言で終わりそうな仕様。

いくつかだけ注意点を紹介:

  • 引数なしコンストラクターのアクセシビリティは、フィールドとして含んでいる構造体のアクセシビリティ以上でないとダメ
  • where T : structwhere T : new() の入れ子に注意
  • オプション引数に注意

アクセシビリティ

以下のようなコードはダメだそうです。

internal struct Internal { }
 
public struct PublicContainsInternal
{
    private Internal _internal;
 
    // このコンストラクターが Internal 構造体よりも広いアクセシビリティなのでダメ。
    // internal とか private なら OK。
    public PublicContainsInternal()
    {
        _internal = new();
    }
}

引数なしコンストラクターの有無で new T() の意味が変わるので、既存の型への引数なしコンストラクター追加は破壊的変更になります。 なので、より広い範囲に公開されてしまうコンストラクターがあると問題を起こしかねないので禁止とのこと。

where T : struct

これまで、構造体は無条件に new T() できていました。 なので、以下のようなメソッドを書いて、CreateStruct<T>() を呼んで実行できないケースは全くありませんでした。

static T CreateNew<T>() where T : new() => new T();
static T CreateStruct<T>() where T : struct => CreateNew<T>();

一方で、Activator.CreateInstance<T>()T 型の引数なしコンストラクターが public でないと例外を起こします。 ということで、もし、C# 10.0 で非 public な引数なしコンストラクターを定義した構造体に対して上記の CreateStruct<T>() を呼ぶと実行時に MissingMethod 例外が出るようになります。 (さすがに、上記 CreateNew<T>() 呼び出しの方をコンパイル エラーにする変更はできなさそう。)

オプション引数

C# のオプション引数で、構造体な引数は default(T) だけを既定値設定できます。 例えば以下のようなコードは new TimeSpan(0) のところだけコンパイル エラーになります。

void M(
    int x = 1, // 組み込み型の場合は const にできるもの何でも OK
    CancellationToken c = default, // default だけは渡せる。この行も OK。
    TimeSpan t = new TimeSpan(0) // これはダメ。一見定数にできそうに見えてもダメ。
    )
{
}

ここで問題になるのは、昔は new T()default(T) は全く同じ意味だったという点。 ということで、以下のコードは有効な C# コードになります。

void M(
    CancellationToken c = new() // new T() と default(T) が同じ意味なので OK。
    )
{
}

で、C# 10.0 では「引数なしコンストラクターを持っている構造体に対しては new T() の意味が変わるので… 以下のような状態になります。

void M(S s = new()) // S に引数なしコンストラクターを足したらコンパイル エラーになる。
{
}
 
struct S
{
    int x;
    public S() => x = 1; // この行の有無で M がコンパイルできるかどうか変わる。
}

ピックアップRoslyn: 文脈キーワードの複雑さ低減

$
0
0

ここ数回やってる「C# Language Design Meeting 議事録が1か月分くらいたまったので1個1個機能紹介」シリーズに見せかけて、もうちょっと別枠。 Design Meeting の場に上がっていなくて、まだ単体の提案ドキュメントが出ただけの状態のものです。

文脈キーワードの判定方法をもうちょっと単純にしたいという話になります。

文脈キーワード

C# は極力互換性を保つ(破壊的変更になるものを避ける)ように進化を続けてきました。 なので、「var というキーワードを使った新機能を追加したいけど、元々 var という名前の型がいた場合は型名を優先して、キーワード扱いしない」みたいなことを毎度やっています。 こういう文脈次第でキーワード扱いになるものを指して文脈キーワード (contextual keyword) と言います。

ということで、以下のようなコードを書いた場合、C# 9.0 現在、1単語目の var はクラスの var になります。

var var = new var();
class var { }

一方で、以下のコードの場合、1単語目はキーワードの var です。 (先ほどのコードとの差はクラス名が大文字始まりの Var な点だけ。)

var var = new Var();
class Var { }

文脈キーワードのわかりにくさ

先ほどの var var = new var(); は割かし悪名高くて、 「互換性を保つため」という理由がなければどう考えても悪い文法です。 IDE 上は型名とキーワードで色が違うのでギリギリ判別は付きますが、 色なしのテキストになったら読めないと思います。

このブログは Visual Studio で書いた結果から型名かキーワードかの「色」を取って書いているので大丈夫なんですが、 そこまでマメなブログがどれだけあるか… 例えば単に Markdown 記法で以下のようなコードを書いたとき、 大体の環境で、var は無条件にキーワード扱いされると思います。

```cs
var var = new var();
```

「文脈を見る」みたいなこと自体学習ハードルを上げてしまうものですし、 挙句、インターネットで調べものをしていて変な色付けで表示されるとなると、 かなり初心者に優しくない状態になっています。

record

ところで、C# 9.0 で導入されたレコード型についてはちょっと条件が違ったりします。 以下のコードはコンパイル エラー。

record record = new record();
record record { }

1単語目の record がキーワード扱いされていて、結果的に、以下の警告・エラーが出ます。

  • 警告: 型名に record は避けてほしい
  • エラー: レコード型の宣言に = は書けない
  • エラー: レコード型の宣言が2つある

ここで、2行目を class に書き換えて、あと、C# 8.0 の文法として成立するように1行目をクラスで覆ってみます。 C# 8.0 ではこれは有効な C# コードでした。

class A
{
    record record = new record();
}
 
class record { }

これを C# 9.0 でコンパイルすると以下のようになります。 色付けが変わって、コンパイル エラーが出ます。

class A
{
    record record = new record();
}
 
class record { }

ということで、実は record キーワードの追加は破壊的変更を起こしています。

ちなみに、文脈キーワードじゃなくなっているわけではないです。 例えば以下のコードはコンパイル可能。 1単語目だけ @ を付けて「キーワードではない」ことを明示しています。 new の後ろなら@ なしでも型名。

@record record = new record();
class record { }

あと、以下の書き方でも大丈夫です。 global:: (global はキーワード)の後ろなので型名であることが明白。

global::record record = new record();
class record { }

record record = new record(); を書けるようにしようと思ったら判定がかなり複雑になるのであきらめたみたいです。

そしてもう1点、互換性に対するポリシーもだいぶ今と昔で違っています。

互換性に関するポリシー

まあ、現実的に、C# で var とか record とかを型名として使うことはありません。 C# では型名は大文字始まりにする文化なので、同じ単語を使うにしても Var とか Recordとかを使います。

それでも、昔だったら「世の中にはどんなコードがあるかわからないので、var 型・record 型が存在する前提で考える」となったと思います。 一方で、今は、「GitHub でコードを探してみたけど9割9分使ってないよ」みたいなのが判断材料になるようです。 実際まあ、この文章みたいに「変なことができる」という指摘をするためのコード以外ではほぼ出てこないと思います。

あと、言語バージョンとターゲット フレームワークのバージョンに関するポリシーも変わりました。 昔は、「古い .NET Framework がターゲットの時でも最新の C# を使えるようにサポートする」という方針でした。 一方で今は、「既定動作では .NET のバージョンによって、それと同世代の C# バージョンに固定される」、 「明示的に C# バージョンを変えることもできるけども、わかってる人だけにやってほしくて、『サポートする』とは言わない」という方針。 基本的には、「C# のバージョンを上げるときには色々と書き換えが発生する覚悟があるはず」、 「保守モードなコードさえ壊さなければまあ許す」という前提になります。

字句解析で文脈判定

話を戻しますが、record キーワードの話。 文脈を見てはいるんですよね。record という名前の型があっても大丈夫です。 ただ、record の場合は以下のような判定をしています。

  • ステートメントとかメンバー宣言の先頭で出てきたらキーワード
  • それ以外の、:: とか new とか他の型名の後ろに出てきたら識別子(型名、変数名、メソッド名等々)

var と何が違うかというと、「型名として使われているかどうか」みたいな情報を必要としていない点。 字句解析の段階(lexical analysis: コンパイラーの仕事のかなり初期の段階)でキーワードかどうかの判定ができます。

(注: 原文が lexical analysis と書いているのでここでもそう書いているものの、 正しくは構文解析(syntax analysis)での判定な気もする。 「その名前の型がある」みたいな情報(semantics: 意味解析)は必要としないものの、 「new の後ろ」とか「:: の後ろ」みたいな情報(syntax)は必要としてそう。 lexical というと普通は new とか :: とかの「単語の切り出し」のこと。)

字句解析だけで判定できるということは、前述の「Markdown が誤判定する問題」も回避できます。 1ファイルだけとか、コードの一部分だけを切り出したものとかを読んでも「キーワードなのか型名なのか」の判別を間違えません。

既存の文脈キーワードも字句解析時の判定に

で、record キーワードの追加で破壊的変更をしたわけですが、それで困った人いる?

さらにいうと、今更 var var = new var(); が書けなくなって困る人いる?

ということで、以下の文脈キーワードの判定方法を変えたい(record と同じく字句解析時に判定)という話が出ています。

これらも完全にキーワードになるわけじゃなく、record と同じく例えば @var var = new var(); なら有効なコードになります。

多分、varnameofdynamic については異論は出ないと思います。 vardynamic はクラス名と、 nameof はメソッド名と競合するんですが、クラス名もメソッド名も普通は大文字開始で書くので。 影響を受けるコードは実用的にはほとんど現れないし、 影響を受けるとしても「@ を足す程度なら許容できる」という範囲に収まる思います。

迷うとすると _ で、_ => _.Name みたいなラムダ式は「聞かないこともない」程度には存在しています。 なのでこいつだけは現状維持の可能性はあります。

まとめ

もう var var = new var(); 書けなくてもいいんじゃない?という話。

メリットとして、GitHub の Markdown などに「コードの一部分だけ記載」とかをやっても型名かキーワードかの誤判定しなくて済むようになります。 (あくまで実装依存ですが、現状の semantics 依存な文法よりはだいぶ実装者にやさしくなります。)

一方で、文脈キーワード自体がなくなるわけではなくて、@record record = new record(); は普通に有効なコードです。 生誕20年のプログラミング言語の「負債の返却」作業としては悪くない落としどころかなと思います。

個人的にはやってほしい変更なので、このブログを読んだ方にぜひとも提案 issue への 👍 をお願いしたかったりします。 (C# チームはある程度 👍👎 の数を見ています。)

ピックアップRoslyn: 分解時の宣言と変数の混在

$
0
0

.NET 6 Preview 1とか Visual Studio 16.9 正式版& 16.10 Preview 1とかが出ましたね。

というの、ライブ配信はしてたんですが。

その中で、今日は C# 10.0 候補で、すでに Visual Studio 16.10 にマージ済みの機能の紹介。 以下のようなコードがコンパイルできるようになっています。

int x;
(x, var y) = (1, "abc");

配信では言ってるんですが、 .NET 6 Preview 1 が出た時点で、コマンドライン (dotnet コマンド)ではコンパイルできていました。 「今回、.NET SDK と Visual Studio で2週間くらいリリースタイミング違うんですね」とか「Visual Studio は Ignite のために取っといたんですかね」とかいう感じ。

で、Visual Studio の方は、16.10 の方で「LangVersion preview」にした時だけ上記コードがコンパイルできます。

分解(C# 7.0)

分解という機能自体は C# 7.0 の頃に入っています。 以下のようなコード、どれも C# 7.0 として有効。

1つ目。() 内で変数宣言。

(int x, string y) = (1, "abc");

2つ目。これを型推論(var)で書いたもの。型推論してる点以外は1つ目のコードと同じ。コンパイラーの解釈結果は全く同じです。

(var x, var y) = (1, "abc");

3つ目。タプル変数宣言。頭に1個だけ var を書いて、複数の変数の宣言をまとめてやる構文。

var (x, y) = (1, "abc");

4つ目。既存の変数を使って分解。

int x;
string y;
(x, y) = (1, "abc");

混在分解(C# 10.0)

で、これの実装時点で、変数宣言と既存変数の混在についても検討はされていました。 「何か地雷を踏みそうで怖い」みたいな感じで「後でやる」扱い。

それが今回、16.10 Preview 1 でマージされました。 以下のコードが通ります。

int x;
(x, string y) = (1, "abc");

変数宣言には var も使えて、それが冒頭のコードになります。

int x;
(x, var y) = (1, "abc");

欲しいかと言われると微妙なライン… と感じますが、 実装負担がほとんどなかったみたいですね。

「あえてエラーにしてたけど、そのあえてエラーにする行を消すだけで動く」というレベルだったみたいで。 コミュニティ(C# チームの外の人)から「エラー行を消して、テストを足しといたよ」っていう pull request が出ていました。

pull request をみるに、式ステートメントと for の初期化式(1項目)中でだけ認めるみたいです。

int x;
 
// OK な例1
(x, string y1) = (1, "abc");
 
// OK な例2
for ((x, string y2) = (1, "abc"); false;)
{
}
 
// ダメな例1
var t = (x, string y3) = (1, "abc");
 
// ダメな例2
m(out (x, string y4));
void m(out (int, string) t) => t = (1, "abc");

ちなみに、コミュニティ貢献であってもレビューのコストは掛かるわけで、 この手の pull request が常にうまくいくわけではないんですが。 今回に関しては pull request 作者さんが元々 charplang/roslyn への貢献が大きい人なのと、 本当に「ほぼテストを足しただけ」レベルの修正だったからあっさりと通ったんじゃないかなという感じはします。

ピックアップRoslyn: using がらみ (using エイリアスの改善と global using)

$
0
0

今日も「C# Language Design Meeting 議事録」の中から1個1個機能紹介。

今日は2/102/22辺りの話になります。

usingがらみに色々更新が掛かるみたいです。 大まかに2点。

  • using エイリアス改善: これまで書けてもよさそうなのに書けないエイリアスを書けるようにする
  • global using: プロジェクト全域に対して有効な using ディレクティブ

global using の方は提案ドキュメントが merge 済みusing エイリアスの話はレビュー中です。

using エイリアス改善

これも細かく言うと3点。

  • キーワードになってる型を直接使えるようにする
  • 配列の []、nullable の ?、ポインターの *、タプルとかを使えるようにする
  • 型引数を持てるようにする

今でも OK なパターンだと以下のような書き方ができます。

using Ok1 = System.Int32;
using Ok2 = System.Nullable<int>;

ところが以下のようなやつは現状ではコンパイル エラー。 ジェネリック型引数の中なら int を書けるのに、直接は書けない。

using Ng1 = int;
using Ng2 = int?;

以下のようなやつもコンパイル エラー。 配列の []、nullable の ?、ポインターの *は現状書けません。

using Ng3 = System.Int32?;
using Ng4 = System.Int32[];
using Ng5 = System.Int32*;

あと、頻出で出ている要望がタプルで、 以下のようなやつも「書きたいのに書けない」筆頭です。

using Ng6 = (System.Int32, System.String); // これがダメな時点でお察しだけど…
using Ng7 = (int, string); // ほんとに書きたいのはこうだし、
using Ng8 = (int id, string name); // 名前付きタプルも書きたい。

この辺り、C# 10.0 でまとめて解消しようという感じになっています。 ちなみに、似たような話だと、enum の基底にキーワードを書けるかどうかみたいなのが C# 6.0 の時に変わっています。

// これなら C# 1.0 の頃から書けた。
enum A : System.Int32 { }
 
// これが書けるようになったのは C# 6.0 から。
enum B : int { }

タプルのエイリアスを付けれるようにしようとなると、まあ、「ジェネリックなエイリアス」も作りたくなります。 これもこの際、C# 10.0 で一緒にやるそうです。

using Fix2<T> = (T, T);
using Fix3<T> = (T, T, T);
using Fix4<T> = (T, T, T, T);

もちろんタプル以外の「ジェネリックなエイリアス」も同じく C# 10.0 で取り組み。

using Option<T> = T ?;

「部分適用」もできるようにしたいみたいです。 以下のような、「2引数のうち片方だけ確定」みたいな「ジェネリックなエイリアス」も作れるようにする予定です。

using StringDictionary<T> = System.Collections.Generic.Dictionary<static, T>;

arity (型引数の数)違いのエイリアスは並べられるようにする予定だそうです。

using MyDictionary = System.Collections.Generic.Dictionary<string, string>;
using MyDictionary<T> = System.Collections.Generic.Dictionary<string, T>;
using MyDictionary<T1, T2> = System.Collections.Generic.Dictionary<T1, T2>;

ちなみに、以下のようなオープン ジェネリック(引数なしの状態)は C# 10.0 でも書けません。

// これは引き続き今後もダメ。
// 空っぽの <> が許されるのは typeof(T<>) だけ
using OpenGeneric = System.Collections.Generic.List<>;

これをやるうえでちょっと悩ましいのが、「制約違反」みたいなのをどこで判定するか。 選択肢は2つあって、1つ目はエイリアスを作る時点 (using T = ...; の行) でエラーにする方法。 エイリアスを「実際にある型」に近い扱いにしようという感じ。 (現状あんまり乗り気ではなさげ。)

using Optional<T> = Nullable<T>; // 「T に制約が付いてないのでダメ」扱いする
using Optional<T> = Nullable<T> where T : struct // と言うことはここに型制約(where)を書けるようにする必要あり

もう1つの選択肢は、「エイリアスの時点では素通し」で、現状、こっちが有力みたいです。 「エイリアスはあくまでエイリアス」で、C 言語のマクロっぽい挙動というか。

using Optional<T> = Nullable<T>; // この時点では T のチェックしない。
 
// Nullable<string> とは書けないので、そのエイリアスの Optional<string> もダメ。
void m(Optional<string> opt) { }

後者が有力なので、using A<T> = T?;null 許容値型になるか、 null 許容参照型になるか、 defaultableになるかはおそらく利用側次第になります。

global using

プロジェクト全域に影響を及ぼす using ディレクティブを書きたいという要望も昔からちらほらあります。

これはまあ、「全域に影響を及ぼす」ってのが怖くてやってなかっただけなんですが、 それももう今更なのかなぁという感じになっています。 と言うのも、

  • null 許容参照型<Nullable>enable</Nullable オプションを与えるとプロジェクト全体で有効・無効が切り替わる
  • SkipLocalsInit[module:SkipLocalsInit] と書けばプロジェクト全体に影響を及ぼせる

みたいな文法がすでにあります。

あと、 ASP.NET なプロジェクトを作るとテンプレート内に _ViewImports.cshtml っていうのが最初から存在しますが、その中身は以下のようになっています。

@using WebApplication1
@using WebApplication1.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

これ、やってることはまさに「プロジェクト中のすべての cshtml に影響を及ぼす using」になります。

あと例えば、最近の C# 公式チュートリアルでは、普通に以下のコードがコンパイルできたりします。 どうも、暗黙的に、SystemSystem.LinqSystem.Collections.Generic 辺りがデフォルトで using されていそうな雰囲気。

Console.WriteLine("Hello World!");

ちなみに、現時点での実装はどうも「ごり押し」っぽい雰囲気があります。 Visual Studio Users Community Japan 勉強会 #6 質疑応答枠 1:12:18~で話したことがあるですが、 おそらく、「書いたコードの前に以下のコードを追加」みたいな実装になっていると思います。

using System;
using System.Linq;
using System.Collections.Generic;
 
class Program
{
    static void Main()
    {

相当に気持ち悪い実装ですが、 こんな気持ち悪いことをしてまで、「using はおまじない」を消したいという状態になっています。

だったら認めようと。

問題は実現方法なんですが、これも初期案としては2案出ていました。

  • <Nullable>enable</Nullable> みたいに、C# コンパイラーに渡すオプション/csproj ファイルに書く設定として提供
  • C# ソースコード中に global using N = int; みたいなのを書けるようにする

ちなみに、後者が有力になっています。global using N = int; 支持になっているのは以下のような理由。

  • Source Generator で使う場合に C# コードで書ける方が助かる
  • dotnet コマンドから csc (C# コンパイラー)に素通ししてあげないといけないオプションがすでに大量にあってあんまりもう増やしたくない
  • 「global using が欲しい」という要望も長年ずっと出続けてる

で、後は文法なわけですが、global using で行くみたいです。

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

まあ、迷うとしたら語順くらいですかね。 普通に global という単語を名前空間にもクラス名にも使えてしまうので、using global だと「キーワードの global か名前空間の global か」の弁別が大変だそうで。

using global;
 
namespace global
{
    class global { }
}

あと、さすがにファイル中散り散りに「プロジェクト全体に影響あり」なものが書かれるのは怖いということで、 global using を書けるのはファイルの先頭(普通の using ディレクティブよりも前)だけにするみたいです。


ピックアップRoslyn: Improved Interpolated Strings

$
0
0

string interplation の改善するって。

現行仕様

C# 6.0 から以下のようなコードで string.Format 相当のことができるようになったわけですが。

var s = $"({a}, {b})";

これは、以下のように展開されます。

var s = string.Format("({0}, {1})", a, b);

これがパフォーマンス的にあんまりよろしくなくて…

特に、冒頭の提案ドキュメントにもある通り、ロギング用途との相性が最悪で、 ILoggerのメソッドがなかなか使いにくそうな感じの引数になっています。

void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);

formatter でラムダ式を渡して、その中で文字列化」みたいなことをしないといけなくて、結構面倒です。

もちろんこのまま使うのは大変なので LogDebug とか LogTrace とかの拡張メソッドには素直に string を引数として受け取るオーバーロードもあったりするんですが、 それがまた罠というか、パフォーマンスにシビアな場面で使ってしまうと露骨に遅くなるという問題が。

遅くなる原因はいくつかあって、

  • 引数(上記の例でいうと ab)を object で受け取ってしまう。引数が値型の時にボックス化を起こす
  • 引数の数が多いと params 扱いになって配列の確保も起きる
  • 即時評価なので、実際には不要なものも(ログレベル的に無視する文字列であっても)必ず文字列化される
  • Span<T> みたいな、C# 7.2 以降、パフォーマンスが重要な場面で多用することになった型を使えない

例えば以下のようなコード(一部仮想コードですが)があった場合、

using System;
 
Log($"{DiagnosticMetric()}, {DiagnosticMetric()}, {DiagnosticMetric()}, {DiagnosticMetric()}");
 
string DiagnosticMetric()
{
    // 診断専用で、日常的に読むには少々重たい値がなにかあるとして
    return その値を返す;
}
 
void Log(string message)
{
    // LogLevel はコンパイル時に確定しない設定ファイルとかから読んだりする想定で
    if (LogLevel < 1) return;
 
    // もし、たいていの場面では LogLevel 0 で運用してるとここにはほとんど来ない。
    // 実際には message を読む必要がない。
    Console.WriteLine(message);
}

以下のように展開されて処理されます。

// ただでさえ「必要な時にだけ呼びたい」というつもりのメソッドが無条件に呼ばれる。
object tmp1 = DiagnosticMetric(); // int → object に代入しててボックス化。
object tmp2 = DiagnosticMetric();
object tmp3 = DiagnosticMetric();
object tmp4 = DiagnosticMetric();
 
// params 用の配列が作られる。
var paramsArray = new object[] { tmp1, tmp2, tmp3, tmp4 };
 
// こういう文字列リテラルもプログラム中に埋め込まれて {0} とかの部分が無駄と言えば無駄。
var format = "{0}, {1}, {3}, {4}";
 
// これも必要性の有無にかかわらず必ず string 生成。
var message = string.Format(format, paramsArray);
 
// 作ったはいいけど、 Log の中で、LogLevel 的に使われない。
Log(message);

IFormattable で受け取ると string 生成は遅らせれる仕様はあるんですが、 あんまりカスタマイズ性もなくて、ボックス化とか params 同様の配列の生成は避けれません。

提案仕様

ということで、以下のように「特定パターンを満たす builder を作って、それの TryFormat メソッドを1個1個呼ぶ」みたいな形に展開できるようにしたいそうです。

Builder.GetInterpolatedStringBuilder(baseLength: 6, formatHoleCount: 4, out var builder);
_ = builder.TryFormat(DiagnosticMetric())
    && builder.TryFormat(", ")
    && builder.TryFormat(DiagnosticMetric())
    && builder.TryFormat(", ")
    && builder.TryFormat(DiagnosticMetric())
    && builder.TryFormat(", ")
    && builder.TryFormat(DiagnosticMetric())
    ;

&& でつないでいるので、1個目で false を返せばもう2個目以降は呼ばれないという実装。 TryFormat にちゃんとしたオーバーロードを増やせば「object を介するせいでボックス化」も避けれます。

「ログレベルに応じて即 false を返す」みたいなのも、以下のような実装でできるようにしたいみたいです。

まず、Logger 自体の定義。 LogTrace メソッドの引数を「特定パターンを満たす builder」にします(この例の場合 TraceLoggerParamsBuilder 型)。

public class Logger
{
    // どこかで設定
    public LogLevel EnabledLevel;
 
    // TraceLoggerParamsBuilder の作りは後述。TryFormat とかを持ってる型
    public void LogTrace(TraceLoggerParamsBuilder builder)
    {
        // TraceLoggerParamsBuilder から文字列を取り出してログ取りする。
    }
}

これで、以下のようなコードを書いたとして、

Logger logger = GetLogger(LogLevel.Info);
logger.LogTrace($"{"this"} will never be printed because info is < trace!");

logger.LogTrace の行は以下のように展開するそうです。

var receiverTemp = logger;
TraceLoggerParamsBuilder.GetInterpolatedStringBuilder(baseLength: 47, formatHoleCount: 1, receiverTemp, out var builder);
_ = builder.TryFormat("this") && builder.TryFormat(" will never be printed because info is < trace!");
receiverTemp.LogTrace(builder);

ログレベルを伝搬できるように、Logger のインスタンスも GetInterpolatedStringBuilder メソッド(builder のファクトリメソッド)に渡せるようにするとのこと。

TraceLoggerParamsBuilder 型は最低ライン以下のように作ります。

public struct TraceLoggerParamsBuilder
{
    bool _logLevelEnabled;
 
    internal static void GetInterpolatedStringBuilder(int baseLength, int formatHoleCount, Logger logger, out TraceLoggerParamsBuilder builder)
    {
        // 実際は baseLength, formatHoleCount とかも使って初期サイズを決定したバッファーとかも作る想定。
        // とりあえず「レベルが合わないログは無視」のためのコードのみ例示。
        builder = new TraceLoggerParamsBuilder { _logLevelEnabled = logger.EnabledLevel <= LogLevel.Trace };
    }
 
    public bool TryFormat(string message)
    {
        if (!_logLevelEnabled) return false;
 
        // バッファーへの文字列書き込み
 
        return true;
    }
}

オーバーロード解決

$"" を渡すときに限り、string のオーバーロードよりも、「特定のパターンを満たす builder」型の方の優先度を高くするそうです。 しかも、$"" がリテラルに展開されい場合だけ。 以下のような挙動になります。

void Log(string s) { ... }
void Log(TraceLoggerParamsBuilder p) { ... }
 
Log($"test"); // {} を含んでないので $ が付かない "test"と同じ扱い → Log(string) の方が呼ばれる
Log($"{"test"}"); // {} の中身が文字列定数なのでコンパイル時に "test" に展開される → Log(string)
Log($"{1}"); // コンパイル時の展開が利かない文字列補間 → Log(TraceLoggerParamsBuilder) 扱いで TryFormat に展開

InterpolatedStringBuilder

Span<T>ArrayPool ベースでパフォーマンスが出るように作った builder を標準提供したいそうです。

現状、InterpolatedStringBuilder という名前で提案されています。

で、string.Format にも以下のオーバーロードを追加。

public class String
{
    public static string Format(InterpolatedStringBuilder builder) => builder.ToString();
}

これで、通常の var s = $"{x}, {y}"; みたいな string interpolation も InterpolatedStringBuilder に対する TryFormat に展開されるようになるとのこと。

その他、考慮する点

その他、以下のような話も。

  • builder 自体、キャッシュしたインスタンスを使いまわすことを考慮してコンストラクターにはしない (GetInterpolatedStringBuilder メソッドを介する)
  • bool TryFormat だけじゃなくて void Format も認めるかどうか
  • stackalloc を使ってバッファーでもヒープ アロケーションを完全になくす案
  • Utf8Formatter みたいにそもそも書き込み先を Span<byte> にする案
  • パフォーマンスを考えると builder は ref struct になるはずで、だったら非同期メソッド内での利用に制限がかかりそう

まとめ

文字列処理、やればやるほど「StringBuilder.Append 直呼びするしかない…」みたいな気持ちになることが多々あるんですが、それがだいぶ解消されそうです。

そこそこ複雑な仕様になっていますが、 現状の ILoggerLog メソッドの実装のしにくさを考えるとだいぶマシかなという感じ。

なんとかしてくれるゼロ幅スペース

$
0
0

200bと打って F5 キー

今の Windows の IME は文字コード直打ちから F5 キーを押すことで任意の文字を入力できる機能を持っています。

いつからだろう。 Windows 10 が「新しい Micorsoft IME」になってからだとは思うんですが、気が付けばそんな機能が。 というか、逆に IME パッドはショートカットキーでは出せなくなった? (右クリック メニューからの選択では出せます。)

昨日の C# ライブ配信中で、「200B だけはよく使う」とおっしゃってる方が要らっしまして。 「ゼロ幅スペースって嫌がらせ以外の用途で使えるの?」、「あえとすさんって実用性ない黒魔術をよく使う人だっけ?」となって「どういう状況で使うんですか?」と聞いた結果が

「Twitter で ASP.NET をリンクにさせない技」

あっ…

それは確かに使うわ…

しかし、文字コード覚えて直打ちする手段に、 F5 なんていうわかりやすいショートカットキーが割当たる時代になったんですねぇ…

追記: その後もうちょっと試して見てる感じ、200B (ゼロ幅スペース)よりも 200D (ZWJ)の方がいいかも。

YouTube で C# がらみのライブ配信初めてました(1年経った)

$
0
0

なんか割かし真面目に YouTube で C# ライブ配信するようになってから、気が付けばもう1年経ってるらしい。

元々、「落ち着いたらちゃんとした告知的なものをここのブログでも書きたい」と思ってたら1年経ちました(今ここ)。

始めた当初の頃とか、ノート PC で無線 LAN で配信してましたからね、これ。 諸事情あって。

配信やるようになった経緯もまあ1回動画にしてますが、そういえばスライドを上げておらず、先ほどようやくアップロード。

ネタの仕入れ

概ねまあ、以下のスライドに全てが詰まっているわけですが。

オンラインでインタラクションが欲しい

「色々と自分の記憶にはあるけども、需要あるのかどうかわからなくて書くモチベーションが湧かない」みたいな「ネタはあるのにネタ切れ」状態だったので、なんかライブ配信でもやってみようかと。

ぶっちゃけて言うと「あれ? なんか Virtual YouTuber の配信が投稿動画型からライブ配信型に入れ替わってない?」みたいなのに唐突に気づいてしまいまして。 これ、自分が配信を始めた2020年の3月とか、さかのぼって言うと配信しようと考え始めた2020年の正月頃なんですけど、その時点でもすでに“今更な話”(にじさんじとかホロライブとかの大手の「箱」ができてからも1年半~2年経ってるくらいの時期)なんですが。 とりあえず、「チャット欄とほぼリアルタイムで会話しながらゲームしてるのすげぇな」とか、 「雑談配信で2・3時間行けるんだ」とかいうことを自分が認識したのが大体その頃。

そしてその結果、「今の自分に必要なのはこのチャット欄じゃない?」とか思ってライブ配信をすることにしました。 普通はまあ、そんないきなりライブ配信を始めたってチャットなんて付かないんですけども。 幸いまあ、「勉強会を開いてた頃から知った名前」な方からのコメントが結構つきまして、なんとか定着して今に至ります。

今目下の悩みとして「新規層はチャットに入ってきてくれるのか」みたいなのはあるんですが…

ネタの書き出し

そしてまあ1年経って。

とりあえずなんかやっとペースみたいなものはつかめてきたかなと。 あんまり頻度多くてもしんどい。 かといってあんまりやらないとなまって再開がしんどい。 折衷案で、「C# 配信かマイクラ作業を垂れ流すだけの配信のどっちかを週1くらいでやる」みたいなところで落ち着けて、C# 配信は2・3週に1回くらいのペース。

(おまけでついてきたマイクラの方はすっかり整地厨に。段差は許さない。)

オレ、段差、ユルサナイ

話を戻して C# 配信の方、当初目的であるネタの仕入れに関しては期待以上の成果を得ていて、むしろアウトプットが全然できていないみたいな状態になっています。

毎度2・3時間しゃべっておいて自分でいうのもなんですが、 文字に書き残しておきたかったりするネタが多いので、 そういう意味では最終成果物である文字メディアが未完成。

毎回、ライブ配信中に書き捨てたコードを GitHub issue に書き残してるんですが。

このメモ書き issue を掘り起こしてちゃんとしたブログにできそうだなとか思うものが、去年の8月くらいの配信までさかのぼれる状態だったりします。

うちのサイトのブログ、2016年とか2018年とか見てもらうとわかるんですが、「1・2年かけて Gist に書き捨ててたコードを発掘するだけで1人アドベントカレンダーができた」みたいな年がちらほらあります。 今、1人 2・3アドベントカレンダー分くらいは書き捨てコード溜まってるんじゃないですかね。

正直、入出力のペースが合っていない(入力過剰)のでさばききれる気もしてないので、 「ご興味がある方はご自由に使ってブログにしてください…」みたいな気持ちにもなっていたりします。

今年、C# 9.0 の新機能ページもまだ埋まってない状態ですからねぇ… (残タスク。まだあと record とか function pointer とかの重たいやつが残ってる。)

当初理念スライドにある「文章メディアはなくならい」の言葉はどこに… (やる気はある)

おまけで宣伝

配信中に急に思い付きで言ってみたことを、そのままじんぐるさんに押し付けたやつがありまして。

C# がらみで日本語で話せる Discord サーバー持ちたいとか思って立てたやつがあります。

立ててから自分では2か月くらい告知出すのもやっておらず、ブログを書いてる今に至ってはもう4か月を過ぎているという。

4か月ほどで、今、約700人くらいのサーバーになっています。

ピックアップRoslyn: ラムダ式の改善

$
0
0

ラムダ式、これまでのバージョンでもこまごまと小さい改善があったりしたので今「lambda improvements」と言われてもタイトル的にはインパクト薄そうですが… C# 10.0 向けに結構大きな改善を入れようとしているみたいです。

背景

C# 2.0 でメソッドをデリゲート型の変数に代入するときに new が要らなくなったのとか、匿名メソッド式が入り、C# 3.0 でラムダ式が入って以来、C# 9.0 に至るまでずっと、デリゲートの型推論の向きはずっとターゲット型からの推論になっています。

C# の文法にはソース型からの推論の方が多いので、デリゲート(特にラムダ式)のターゲット型推論の挙動を「何か変」と思う人は多いんじゃないかと思います。 一番多いのは、「以下のコードがコンパイルできないのは変じゃない?」みたいに思うこと。

var f = (int x) => x * x;

大体は、デリゲートが「同じ引数・戻り値でも別の型を作れるし、それらは互いに区別する」という仕様のせい。 ターゲットの方の型が決まらないとラムダ式の型を決定できません。

using System;
 
A a = (int x) => x * x;
B b = (int x) => x * x;
Func<int, int> f = x => x * x;
 
// A, B, Func<int, int> は同じ引数、同じ戻り値だけど別の型。
// 互いに代入不可。
a = b;
b = f;
f = a;
 
delegate int A(int x);
delegate int B(int x);

今でこそ「ターゲット型がわからないときは Func<T, TResult> にすればいいんじゃない?」みたいなことを言われますが…

確かに今なら「できる限りは Func<T, TResult> を使う」みたいな習慣ができていますが、 Func<T, TResult> が標準ライブラリに入ったの自体が C# 3.0 (後付け)だったりするので、 「デフォルトは Func<T, TResult>」というほど盤石な地位かと言うと微妙に悩んだりします。

あと、C# のジェネリクスの制限のせいで、Func には以下の問題もあったりします。

  • Func<ref int, readonly ref int> みたいな、ref (参照渡し)な型引数を作れない
  • Func<T1, ..., T16, TResult> みたいな、引数の個数違いを1個1個書く必要があって、標準ライブラリで提供しているものは最大で16引数しかない

あと、まあ、ラムダ式の最大の用途である LINQ が「ターゲット型推論だけあれば十分」なのもあります。

using System.Linq;
 
var q = new[] { 1, 2, 3, 4, 5, 6 }
    .Where(x => (x % 2) == 0) // ターゲット型推論で Func<int, bool> に決定
    .Select(x => x * x);      // ターゲット型推論で Func<int, int> に決定

一方で、最近、 ASP.NET 方面から「任意のデリゲートを受け付ける Map メソッドを作りたい」という話が上がっています。

例えば、以下のような短い書き方で所定の URL に対するアクションを登録できるようにしたいそうです。

builder.MapGet("/", () => { });
builder.MapGet("/category/{c}", (char c) => char.GetUnicodeCategory(c));

C# としてもこの路線は支持したいそうで、 そうなると3点ほどラムダ式に変更を入れたいとのこと。

  • ラムダ式に属性を付けれるようにする
  • ラムダ式の戻り値を(ターゲット型推論じゃなく、ラムダ式側で)明示できるようにする
  • (ターゲット型がない時の) ラムダ式の「自然な型」を導入する

属性

C# 9.0 でローカル関数に属性を付けれるようにしたわけで、ラムダ式にも属性を付けれるようにしてもいいんじゃないかという話があります。 あと、同じく C# 9.0 でラムダ式に static を付けても大丈夫だったので、だったらさらに [] が付いても大丈夫っぽい。

名前通り「式」(どこにでも書ける構文)なのであんまり長いものは書きたくはないですが、 例えば以下のようなコードは十分に「書ける範囲」かと思います。

app.MapAction([HttpGet("/")]() => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")]([FromBody] Todo todo) => todo);

C# 10.0 でこれらを認めたいそうです。

明示的な戻り値の型

ラムダ式、C# 3.0 の頃からの構文でも、引数の型は明示できます。 一方で、戻り値の型は常に推論任せだったわけですが、いい加減明示する方法が欲しいそうです。

例えば戻り値の型を明示できなくて困る?例は以下の通り。

using System;
 
var a = m(x => x * x, 1); // これはターゲット型推論任せで int, int に決定。
var b = m((short x) => x * x, 1); // これが実は型決定できなかったり。
 
T2 m<T1, T2>(Func<T1, T2> f, T1 x) => f(x);

この例の場合は以下のように型引数を明示することで一応は解決しますが、これが書き心地いいかと言われると微妙な感じ。

var b = m<short, long>(x => x * x, 1);

ジェネリクスの場合は型引数の方を明示するという手段を取れますが、 後述する「自然な型」を導入しようと思うと戻り値の型の明示がないと困る場面が出てきます。 ということで、以下のような「引数リストの後ろに : T を書く」という文法で戻り値の型を明示できるようにしたいそうです。

var b = m((short x) : long => x * x, 1); // ラムダ式の戻り値の型を明示的に long にする。

デリゲートの自然な型

最後が一番大きな変更になるんですが、ラムダ式を varobjectDelegate に代入できるようにするために、 「ターゲット型がわからないときはこの型を選択する」みたいなもの(natural delegate type: デリゲートの自然な型)を決めておくことにするそうです。

候補が搾れる分にはラムダ式やメソッドからデリゲートの型を自動決定。

ラムダ式の例:

var f1 = () => default;        // error: no natural type (決定不能)
var f2 = x => { };             // error: no natural type (決定不能)
var f3 = x => x;               // error: no natural type (決定不能)
var f4 = () => 1;              // System.Func<int>
var f5 = () : string => null;  // System.Func<string>

メソッド グループの例:

static void F1() { }
static void F1<T>(this T t) { }
static void F2(this string s) { }
 
var f6 = F1;    // error: multiple methods (F1() と F1<T>(T) の区別が付かない)
var f7 = "".F1; // System.Action
var f8 = F2;    // System.Action<string>

で、「自然な型」は以下のように作るそうです。

  • System.ActionSystem.Func を使えるとき(ref とかが付いていなくて16引数以下のとき)はそれを選ぶ
  • 非同期メソッドの場合、戻り値の型は Task/Task<T> (System.Threading.Tasks 名前空間)にする
  • System.ActionSystem.Func を使えないときは、匿名型みたいに、 internal な匿名のデリゲートを作る

匿名の型を作ってしまうのは、ASP.NET の MapAciton みたいに Delegate に代入して MethodInfo を見て分岐する用途では十分そうです。

一方で、例えば以下のような is 分岐はできないはずなので、この用途には使えません。

using System;
 
// これはターゲット型からの型推論で成り立っているので OK。
a((ref int x) => ref x);
 
void a(RefDelegate d) { }
 
// C# 10.0 でコンパイルできるようにしたい書き方。
// この行はコンパイルできるようになる。
// Func<ref int, ref int> とは書けないので、匿名型が作られる。
b((ref int x) => ref x);
 
void b(Delegate d)
{
    // (ref int x) => ref x は匿名のデリゲート型が作られてその型になる。
    // delegate ref int Anonymous1$(ref int x) みたいな「通常の C# コードでは書けない名前の型」になるはず。
    // 例え引数・戻り値の型が一致していても、RefDelegate とは違う型なので、is RefDelegate が true になることはない。
    if (d is RefDelegate r)
    {
    }
}
 
delegate ref int RefDelegate(ref int x);

ピックアップRoslyn 4/4: static virtual/abstract members

$
0
0

インターフェイスの静的メソッドを virtual/abstract 指定できるようにする話が出ています。

主な用途は、

  • ファクトリ
  • 比較 (Equatable とか Comparable)
  • 数値計算

とかになると思います。 一番求められている用途は数値計算で、要は NumPy みたいなことを C# でも苦痛なく、かつ、パフォーマンスを損なうことなく実現したいというものです。

ファクトリ

数値計算に特化した仕様かと言うとそんなこともないので、先に他の用途について触れておきます。

ジェネリックなメソッドを作るとき、new() 制約を付けることで引数なしのコンストラクターなら呼び出せるんですが…

void m<T>() where T : new()
{
    var x = new T(); // OK
}

ところが、この new には引数を渡せません。

void m<T>(int i)
    where T : new(int) // こう書きたい(ダメ)
{
    var x = new T(i); // ダメ
}

これを例えば以下のように書けるようにすることで代替できるようになります。

void m<T>(int i)
    where T : IConvartibleFromInt // 普通のインターフェイス制約
{
    var x = T.New(i); // こう書けるようにする
}

interface IConvartibleFromInt
{
    public static abstract IConvartibleFromInt New(int i);
}

generic math

たびたび出てくる要望として、 +, -, *, / をジェネリックな型で使いたいというものがあります。 わかりやすい例だと「Enumerable.Sum の実装何個あるんだ」って話で。 中身はほぼ定型文で、以下のようなコードのコピペが何個も並んでいます。

foreach (int v in source)
{
    sum += v;
}

コピペせざるを得ないのはジェネリックな型に対して + を使えないからです。

業務アプリ開発とかでは大体 intdouble、せいぜい decimal を使っておけばいいのでジェネリックじゃなくてもそこまで困らないんですが、 汎用数学ライブラリみたいなのを作ろうとすると結構困ります。 NumPy みたいなものの利用者を取り込みたいし、この問題を解決したいという流れ。

現状の C# で汎用数学処理を書こうとするとどうなるかと言うと、以下のような感じ(3年位前のブログ):

ブログタイトルが「型クラス」となっていますが、まあ、それが今回出ている「static virtual 提案」の原型。

この「Shapes」というやつは結構込み入った仕様なんですが、 いったんこのうちの一部分というか、既存の文法からそう大きく外れない範囲でできるものが 「インターフェイスの static メソッドに virtual/abstract を認めよう」というものです。

上記の Sum であれば、「0 を取得」と「足し算」の2つがあれば書けるので、まず以下のようなインターフェイスを用意。

interface IAddable<T> where T : IAddable<T>
{
    static virtual T Zero { get; } => default(T);
    static abstract T operator +(T t1, T t2);
}

これが入るのであれば、標準の int 型(Int32 構造体(System 名前空間))に以下のような実装も足されることになります。

struct Int32 : …, IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

これを使って Sum メソッドを書くと以下のようになります。

public static T Sum<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

これ、下手な実装をするとパフォーマンスを著しく損ねます。 + なんてネイティブコード化されると CPU の1命令だったりするわけですが、 そこに、インターフェイスが挟まることで仮想関数呼び出しが挟まったり、インライン展開阻害が起きたりして数倍~1桁遅くなります。

とはいえ、前述の3年前のブログでやっているような「値型ジェネリクスを使った黒魔術」でパフォーマンスは解決できるんですが、型引数が余分に1個増えたり、演算子を使えなかったり、だいぶ使い勝手は悪いです。

public static T Sum<T, TAddable>(T[] ts) where TAddable : IAddable<T>
{
    T result = default(TAddable).Zero;
    foreach (T t in ts) { result = default(TAddable).Add(result, t); }
    return result;
}

型引数による分岐

普通の、既存の virtual/abstract メソッドの場合、 実際にどのメソッドが呼び出されるかはインスタンスの実行時の型によって決まります。

using System;
 
// 型引数が何だろうと、インスタンスが A なので表示されるのは "A"。
m<I>(new A());
m<A>(new A());
 
// 型引数が何だろうと、インスタンスが B なので表示されるのは "B"。
m<I>(new B());
m<A>(new B());
m<B>(new B());
 
void m<T>(T x) where T : I => x.M();
 
interface I
{
    void M();
}
 
class A : I
{
    public virtual void M() => Console.WriteLine("A");
}
 
class B : A
{
    public override void M() => Console.WriteLine("B");
}

一方、static virtual/abstract の場合は型引数を見ます。 コンパイル時に決定済み。 abstract なままのもの(実態がないもの)を使うとコンパイル自体できません。

using System;
 
// static virtual/abstract の場合は型引数の方で呼び出し先が決まる。
m<I>(new A()); // コンパイル エラー。 I.M に実装がない。
m<A>(new A()); // "A"
 
m<I>(new B()); // コンパイル エラー。 I.M に実装がない。
m<A>(new B()); // "A"
m<B>(new B()); // "B"
 
void m<T>(T x) where T : I => T.M();
 
interface I
{
    public abstract static void M();
}
 
class A : I
{
    public override static void M() => Console.WriteLine("A");
}
 
class B : A
{
    public override static void M() => Console.WriteLine("B");
}

型システムの修正

これ、C# コンパイラーのレベルで実現しようと思うと、多分前述の「黒魔術的な構造体ジェネリクス」みたいなコードを生成することになります。 さすがにちょっと「コンパイラーが裏でこっそり生成するコード」にするのもためらわれる(型引数の個数が変わるとかだいぶつらい)レベルです。

なので、.NET ランタイムの型システム自体に手を入れる必要がありました。 実際、型システムに手を入れる(.NET 5 以前では使えない機能になる)方向で実装を進めるそうです。

C# 8.0 くらいから、こういう「古いランタイムでは動かない機能」がちらほら入ってきています。

(ちなみに、この辺りの一定バージョン以上のランタイムでしか動かない機能については「RuntimeFeature」でちょっと書いています。)

とはいえ、デフォルト実装とか共変戻り値と比べても、static virtual/abstract は実装が難しめの機能になります。

結構な大事なんですが、Miguel de Icaza (Mono 創設者)がプロトタイプを作っていて、これをベースに話が進んでいるみたいです。

Visual Studio 16.11 Preview 2: record struct と global using

$
0
0

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

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

record struct

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

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

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

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

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

struct と record struct

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

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

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

struct と with

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

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

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

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

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

global using

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

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

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

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

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

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

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

global using Date = System.DateOnly;

通常 using と同列

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

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

global using System;

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

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

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

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

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

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

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

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

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

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

using System;

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

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

問題を起こせる範囲

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

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

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

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

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

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

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

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

C# 10.0 に入れるかどうか確定させる時期が来たようです

$
0
0

今年もそろそろ、どの機能を C# 10.0 にして、どの機能を "Next" のまま(11 以降に先送り)にするかを決めないといけない時期が来ましたと言う話。

マージ済み機能

まず、Language Feature Status が更新されました。

「C# 10.0」の方に移ったのが以下の4つ。 (17.0p2 が Visual Studio 17 Preview 2、 17.0p3 が Preview 3。)

機能 Merge 先
Lambda improvements 17.0p2
Static Abstract Members In Interfaces 17.0p2
Interpolated string improvements 17.0p3
File-scoped namespace 17.0p3

※ これだけ「Preview」です(後述)。

Visual Studio 17 Preview 1 が出てからそろそろ1か月くらいですし、 このリストに「p3」(次の次)の文字が並び始めたんで、そろそろ Preview 2 が出るんでしょうね。

10.0 には間に合わせるリスト

それとは別に、7月12日の LDM では残りの "Next" について、10.0 に入れるべきかどうかの話があったみたいです。それによれば、

だそうです。

あと、ひっそりと、raw string literals の実装コードの中に「C# 11.0」の文字が (コンセプト検証用のコードですけど、「既成事実化」を狙ってそうな匂いが多少)。

※ Preview

これまでだいたい、Preview というと、

  • 1月~7月くらいまでの間、LangVersion preview 指定必須で機能提供
    • .NET 6 Preview を使っていても LangVersion preview 指定が必須
  • 8月くらいから、時期 .NET SDK と同期して LangVersion default 扱い
    • .NET 6 Preview を使うと LangVersion default が C# 10.0 になる
  • 11月の .NET SDK リリースに合わせて言語機能もリリース
    • .NET 6 のリリースと同時に C# 10.0 としてリリース

みたいなものしかありませんでした。

ただ、今回、 .NET 6 リリース時点でも Preview として残りそうな機能が1個あります。

こいつだけは、 Visual Studio 17 Preview 2 時点で動く物が世に出るものの、.NET 6 リリース時点でも LangVersion preview 必須になりそうです。 要するに、Preview である期間を十分長く取りたいくらいチャレンジ度合いの高い機能です。

C# 側だけでなく、 .NET ランタイム側にも Preview オプション指定での実行が必要だし、RequiresPreviewFeatures 属性が付いていて「ランタイム側の Preview 機能を使う前提」のライブラリでしか使えないようにアナライザーでチェックを行うみたいです。


.NET 6 Preview 7 & Visual Studio 2020 Preview 3

$
0
0

一昨日くらいに来てました。

当日、このネタでライブ配信:

「一気に情報が来ても小一時間では話しきれない」って感じで極々一部しか話せませんでしたが。

「Visual Studio 2020 Preview 3 の方が CDN トラブルで配信が1日延期」というトラブルに見舞われ、 「SDK だけを先に .NET 6 Preview 7 に上げてしまうと、標準のテンプレートがコンパイル エラーを起こす」という事件もありましたが、1日経って問題は解消済みです。

とりあえず、ブログとしては「今回入った C# 10.0 機能」の話を書こうと思います。 ちなみに、今回の更新でほぼ C# 10.0 の全機能が入っています。 (1個だけまだなものがあるけども、「10.0 リリース時点で preview 機能として残る」判定を受けている機能なので、非 preview な 10.0 機能は全部 merge 済み。)

(全機能一覧はトラッキング issue を立ててるので現状そちらを見ていただけると。)

.NET 6 Preview 7 での C# 10.0 新機能

Language Feature Statusで Merged into 17.0 と 17.0p3 になっているやつが今回入っています。 (17.0 になってる2つはもっと前に入ってた疑惑ちょっとあり。 Visual Studio 2020 Preview 2.1 のときかも。)

以下の6つ。

あと、Lambda improvements も1個前の Preview では動いていなかった機能が増えているので、合計7つ。

Improved Definite Assignment

C# には元々、確実な代入ルールってのがあって、「未初期化変数から未定義な値を取り出す」みたいなことはできない仕様になっています。

int x;

Console.WriteLine(x); // コンパイルエラー

if (int.TryParse(Console.ReadLine(), out x))
{
    // ここでは x が初期化済みな保証があるのでエラーが消える。
    Console.WriteLine(x);
}

これのためのフロー解析に改善の余地があることが周知の事実で長らく手つかずだったんですが、それが C# 10.0 でちょっと改善します。

これまで ?. とか ?? とか ? : が絡むときの解析が甘くて、過剰にエラーになっていました。 それが緩和されて、例えば、以下のようなコードがコンパイルできるようになっています。

using System.Diagnostics.CodeAnalysis;

m(null);
m(new R<string>(null));
m(new R<string>("abc"));

void m(R<string>? x)
{
    if (x?.TryGetValue(out var v) == true) // ここの var v の definite assignment 判定が改善された。
    {
        Console.WriteLine(v.Length); // 前までこの行がエラーになってた(C# 10.0 から OK に)。
    }
    else
    {
        Console.WriteLine("null");
    }
}

record class R<T>(T? Value)
{
    public bool TryGetValue([NotNullWhen(true)] out T value)
    {
        if(Value is { } v)
        {
            value = v;
            return true;
        }
        else
        {
            value = default!;
            return false;
        }
    }
}

Extended property patterns

プロパティ パターンで、 多段のメンバーを . でつないでマッチングできるようになりました。

var x = new A(new B("a"));

if (x is A { X.Value.Length: 1 })
{
    Console.WriteLine("len 1");
}

record A(B X);
record B(string Value);

Interpolated string improvements

文字列補間のパフォーマンスが大幅に向上します。

以下のようなコードがあったとして、

Console.WriteLine(m(1, 2, 3, 4));

string m(int a, int b, int c, int d) => $"{a}.{b}.{c}.{d}";

これまでは string.Format("{0}.{1}.{2}.{3}", new object[] { a, b, c, d }) に展開されていました。 それが、所定の条件を満たせば(普通にやってれば .NET 6 をターゲットにして C# 10.0 でコンパイルすると)、以下のようなコードに変化します。

var h = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(3, 4);
h.AppendFormatted(a);
h.AppendLiteral(".");
h.AppendFormatted(b);
h.AppendLiteral(".");
h.AppendFormatted(c);
h.AppendLiteral(".");
h.AppendFormatted(d);
return h.ToStringAndClear();

ちなみに、C# コンパイラーのレベルで頑張っていることなので再コンパイルが必要です。 これに関しては「既存のコンパイル済みプログラムを .NET 6 で動かすだけで速くなる」みたいなことはないです。

File-scoped namespace

いままで:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class A
    {
    }
}

これから:

namespace ConsoleApp1;

class A
{
}

「たかが1インデント」と言われてたやつなんですが… まあ確かにこの1インデントが深い言語の方が、今となっては少なく。

Parameterless struct constructors

さかのぼること C# 6.0 の時に、Activator のバグでできなかったやつ、再チャレンジ(成功)。

構造体のフィールドでも非 null 保証とかがやりやすくなります。

struct A
{
    public string S { get; } = "abc"; // 前まで初期化子を書けなかった
}

struct B
{
    public int[] Array { get; }
    public B() => Array = new int[4]; // 前まで B() を書けなかった
}

まあ、default からは逃げられないんですが…

// これは大丈夫。引数なしコンストラクターで new int[] されてる。
Array4 a = new();
Console.WriteLine(a[0]);

// default は引数なしコンストラクターを呼ばない。
a = default;
Console.WriteLine(a[0]); // ぬるぽ

struct Array4
{
    private readonly int[] _array;
    public Array4() => _array = new int[4];
    public int this[int index] => _array[index];
}

Caller expression attribute

CallerInfo 系の属性に新しい仲間が増えました。

CallerArgumentExpression 属性で、「引数に渡した式」を取れるようになります。

using System.Runtime.CompilerServices;

m(2 * 3 * 4); // 2 * 3 * 4 = 24

var (x, y, z) = (1, 2, 3);
m(x + y + z); // x + y + z = 6

static void m(int result, [CallerArgumentExpression("result")] string? expression = null)
{
    Console.WriteLine($"{expression} = {result}");
}

主にロギング用途になると思います。

Lambda improvements

.NET 6 Preview 6 時点で以下のようなコードは書けていたんですが。

Delegate f = int (int x) => x * x;

Prevew 7 から以下のようなコードも書けるようになりました。

var f = int (int x) => x * x;

この場合、f の型は Func<int, int> になります。 System.ActionSystem.Func が使える場合にはそれを、 使えない場合には internal なデリゲート型をコンパイラー生成して使うそうです。

デリゲートの仕様上、以下のような挙動をするのでその点には注意が必要です。

// これは target-typed 型決定で、Predicate<int> になる(コンパイル可)。
m(x => x == 0);

// 一方で、これは f の型が Func<int, bool> になる。
var f = (int x) => x == 0;
m(f); // Func<int, bool> を Predicate<int> に変換でしません(コンパイル エラー)。

static void m(Predicate<int> f) { }

最初の C# プログラム(.NET 6 新テンプレート)

$
0
0

.NET 6 ではプロジェクト テンプレートが更新されて、かなりシンプルになります。 例えば、コンソール アプリの場合(dotnet new console コマンドで生成)は(コメント行を除けば実質)以下の1行だけの C# ファイルが生成されます。

Console.WriteLine("Hello, World!");

先日の .NET 6 Preview 7 から、コンソール アプリと Web アプリがこの新テンプレートになっています。 トラッキング issue を見るに、他のタイプのプロジェクトも同じ方針で書き換え中みたいです。

今日はこの新テンプレートがらみで、背景とか、内部挙動的な話とか、Preview 7 から正式リリースまでの間に掛かる予定の変更の話とか。

旧テンプレート

まあ、これまでのテンプレートが以下のようなものでしたから、ずいぶんとすっきりしました。

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

C# の勉強を続けているとそのうちどこかで出会うことになるコンセプトとはいえ、完全な初心者にとってはノイズにしかならない「おまじない」が結構あります。

ということで、「初心者向けにもっとシンプルに書ける言語になりたい」というのが結構前から C# の課題でした。

C# 6.0 の時にスクリプト実行用の構文ができたりもしましたし、 C# 9.0 ではトップ レベル ステートメントが導入されました。 その流れで、C# 10.0 では global usingfile-scoped namespace などが入りましたし、 .NET 6 以降、標準のプロジェクト テンプレートも積極的にこれら「シンプル化のための機能」を使ったものになります。

「最初の C# プログラム」構想

今時のプログラミング言語は大体公式サイトで「ブラウザー内で試してみる」機能が付いています。

というか「プログラミングを始めるにはまず Visual Studio をインストールします」なんて言うと、今時そこで9割の人が離脱します…

ということで、C# も現在では公式サイトで、

とたどって、ブラウザー内で C# コードを試してみることができます。 そこで表示されているのが「新テンプレート」にもある以下の1行。

Console.WriteLine("Hello, World!");

今まで、この1行、実は正規の C# だと動かなかったんですよね。 「適切な場所にこの C# をコピペすれば動く」と言う意味ではちゃんと動く C# コードなんですが、 「この1行だけをコピペしても動かない」という意味で「動かないコード」でした。

ところが、この公式サイト内では以下のように「動いてます」。

最初の C# プログラムをブラウザー内で実行

(そして、「公式サイトで動くことになっているコードが手元では動かない」と言うようなクレーム、結構あると思うんですよね。 僕も Twitter 上でこの手の文句を言っている人を数度見たことがあり。)

これ、最初は「C# スクリプト(通常の C# とちょっと別文法)なのかな?」とも思ったんですが、 以前ちょっと試した感じ、 どうもスクリプト用の構文ではなさそうと言う結論に。

少なくとも今日(2021年8月16日)の時点では、おそらく、前後に以下のコードを文字列結合してからコンパイルしているのだろうという推測をしています。

前:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Program
{
    class Program
    {
        static void Main()
        {

後ろ:

        }
    }
}

その結果、以下のようなおかしな真似ができます。

おかしな対話型 C# コード

「最初の C# プログラム」を動く C# コードに

で、C# 10.0 でようやくこの「最初の C# プログラム」が動くコードになりました。

C# 9.0 時点でも以下のコードまでは書けていたんですが…

using System;

Console.WriteLine("Hello, World!");

ここで最後のノイズが using System; だったわけですが、 これが C# 10.0 の global using で解消します。

要は、「暗黙的に using してる名前空間がある」という情報を何らかの方法で C# コンパイラーに伝えられればいいんですが、C# 10.0 では以下のような方式を取ることにしました。

  • global using という C# の文法を追加
  • .NET SDK 側で、コンパイル開始時に所定の global using コードを生成

例えば、コンソール アプリの場合、以下のようなソースコードが自動的に作られて、 手書きコードと一緒にコンパイルされます。

// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

.NET 6 Preview 7 でのバグ挙動と破壊的変更問題

ただし、先日リリースしたての .NET 6 Preview 7 ではこれのせいでいくつか問題を起こしています。 すでに対処はされていて、次のリリース(おそらく RC 1)までに治るっぽいです。

とりあえずは Preview 7 時点のバグ挙動の説明。

上記の global using コードをどういう条件で生成するかと言うのが問題でして、 Preview 7 時点では「TargetFramework が net6.0 なら生成」となっていました。 これでどういう問題を起こすかと言うと、

  • TargetFramework net6.0 なのにあえて LangVersion を9以下に設定して global using を使えなくする
    • 「global using を使うなら C# 10.0 以降を使え」というエラーが出る
  • TargetFrameworks で複数の TargetFrameworks を混在させる
    • net6.0 のときだけ global using が掛かっている状態になるので、net6.0 のときにしかコンパイルできないコードが生まれる

そして、TargetFramework を net6.0 に変えるだけで、既存のコードを壊すことがあります。 まあ、勝手に using している名前空間が足されている状態なので、そこに含まれている型と名前被りがあると「不明瞭な型」扱いを受けてコンパイル エラーを起こします。

例えば、.NET チーム自身が地雷を踏んで、暗黙的な global using 生成を止めるという一時的な対処をやっています。

当初はこれを「.NET 6 での破壊的変更」としてユーザーに受け入れてもらおうと思っていたみたいなんですが、さすがに無理だと思ったのか、方式を改めるみたいです。

次のリリースまでに掛かる変更

ということで、変更計画 (6.0.100-rc.1 に入れる目標とのこと)。

今(Preview 7 時点):

  • net6.0 だと自動的に global using コードが生成されてしまう
  • それを抑止するために、DisableImplicitNamespaceImports というオプションがある(本来は Visual Basic 用)
  • プロジェクト テンプレートにはこれ関連の項目は何も含めない

変更後(RC 1 目標):

  • ImplicitUsings というオプションを用意して、これが true もしくは enable の時だけ global using コードを生成する
    • DisableImplicitNamespaceImports は C# 向けにはなくす(Visual Basic 用オプションの流用をやめる)
  • プロジェクト テンプレートに <ImplicitUsings>enable</ImplicitUsings> の行を足す

明示的なオプション指定を必須にして、標準のテンプレートにこのオプションを足すという方向性。

なので、既存のプロジェクトを net6.0 に変えただけで破壊的変更ということにはならなくなる予定です。 その代わり、csproj (プロジェクト設定ファイル)に1行ノイズが増えますが… これはさすがにしょうがなさそう。

.NET のカルチャー依存 API 問題

$
0
0

以下のコード、実行環境によって出力結果が変わります。

Console.WriteLine(new DateTime(2021, 8, 22));

日本語 Windows 環境だと 2021/08/22 0:00:00 と表示されると思いますが、 OS 設定でカルチャーを変更すると別の書式になります。 例えば、en-US カルチャーにすると 8/22/2021 12:00:00 AM になります。 要するに、DateTime.ToString は OS のカルチャー依存になっています。

問題点はいくつかあるんですが…

  • ToString みたいなよく使うメソッドの既定動作がカルチャー依存
  • WebAssembly みたいな、カルチャー情報を使いたくない環境がある
  • カルチャー非依存にしたければ北米カルチャーを強要されがち
  • 北米カルチャーが思った以上に世界から浮いてる

今日はこの辺りの話を書きたいと思います。

なぜ今

国際化対応をしたことがある人ならたぶん、 .NET Framework 1.0 リリース当初であったり、 さらに言うと C# のリリース前、Windows アプリは Visual Basic (無印)や C++ を使って書いていた頃からこの手の問題には悩まされていたと思います。

ところが最近(去年くらいから)、別に国際化対応をしなくても悩むことが出てきました。

ICU 化

元々、(Windows 向けの) .NET Framework では NLS (National Language Support)という Windows 組み込みの多言語対応データを使って国際化対応をしていました。

一方、マルチプラットフォームの .NET Core (.NET 5 以降は単に「.NET」と呼ぶようになったやつ)が出たことで、 Windows 以外では ICU (International Components for Unicode) という Unicode 標準に基づくライブラリを使うことになりました。 多くの Unix 系 OS では標準搭載、あるいは、割かし簡単な手間で ICU を組み込むことができるようになっています。 Windows 10 でも、2017年のアップデート以降、標準で ICU が入るようになりました。

.NET Core 3.1 までは、実は、Windows は NLS を、他の OS では ICU を使っていたことで、 実行環境によって微妙に実行結果が変わることがありました。 この挙動があまり望ましくないということで、 .NET 5 (2020年11月リリース)からは Windows でも ICU を使うように変更されました

その結果、.NET Core 3.1 から .NET 5 にアップデートすると、微妙に文字列処理に変化があったりします。 (NLS に戻すオプションもあるので、まずそうならそのオプションを指定することになります。)

軽く騒動もあったんですが、 そこで初めて、「えっ、このメソッドもカルチャー依存だったんだ…」みたいな事実に気づいたという方も多いんじゃないかともいます。

ちなみに日本語でも、「IgnoreCase を付けると "つ" と "っ" が Equals 扱いになった」みたいなことが起きています。 (.NET 5 リリースのから今年7月まで気づかれてなかったっぽい。)

OS 搭載の ICU に頼れない環境

ICU のデータはフルに持つと結構なでかさになります。 .NET アプリの実行に必要な分だけ抜き出しても 1.4MB くらいあります。

まあ 1.4MB くらいのサイズであれば、サーバー OS ではそれほどきついサイズではないので、「たいてい OS が持ってる」を前提にしても問題ありませんでした。 問題は iOS や WebAssembly 実行で、 これらの環境ではアプリごとに ICU データを同梱して配布する必要があります。 WebAssembly なんかはブラウザーでダウンロードして実行する前提なので、 1.4MB もバカにならないサイズになります。 (だいたいはブラウザー自身、iOS であれば WebKit が ICU 依存なので、データとしては ICU を持っているはずなんですけどね… そのデータをアプリ開発者が参照する手段はないです。)

ということで、.NET も Blazor WebAssembly とかに注力している昨今、 「カルチャー依存をなくしたい」という要望も強くなってきました。

Blazor では、

  • EFIGS (西欧向けのフランス語、イタリア語、ドイツ語、スペイン語だけを持つ)のデータだけを持つ
  • CJK (データがでかくなりがちな日中韓)のデータだけ抜く
  • 完全にカルチャーを抜く(CurrentCulture を取ろうとしても InvariantCulture が返ってくる)

みたいなモードが選べるようになっていたりします。 完全にカルチャーを抜くモードは Blazor に限らず、InvariantGlobalization というオプションを指定することでどのタイプの .NET アプリでも同じモードで実行できます。

謎のカルチャー依存

ということで今になってカルチャー依存問題を踏んでいるわけですが。 ここでもう1個問題になるのが、「えっ、このメソッドもカルチャー依存だったの?」みたいな意外さ。

冒頭の例でも書きましたが、ただの ToString すらカルチャー依存です。 .NET が Windows 限定で、しかもアプリと言えば Windows GUI アプリだった時代にはこれでもよかったんですが…

当然ですが、Web アプリだとこの挙動は結構イラっとします。 クラウド インスタンスを新規で立ててカルチャー設定を忘れて ToString の結果が変わるみたいなやつ。

先ほど「軽く騒動もあった」と紹介したやつなんて結構ひどくて、「IndexOfContains で結果が違う」という状態です。 なんでこんなことになるかというと、IndexOfCurrentCulture 依存で、ContainsOrdinal 比較だからです。 混ざってるのはさすがにつらい…

古くからある API ほどカルチャー依存で、新しめの API は Ordinal もしくは InvariantCulture 利用に変わっています。

ちなみに、「実行環境によって結果が変わる」という問題がある他にも、 パフォーマンス上の差もあります。 そもそも CurrentCulture 取得がそこそこ負担が掛かる処理というのもありますし、 Ordinal (文字コード通りに単なる数値比較するだけ)と比べてカルチャー依存処理("a" と "à" や、"か" と "が" 等の関係性を考慮)する方が重たいに決まっています。

カルチャー依存の API を呼ぶときは明示的にカルチャー指定しろよ」というアナライザーがあったりするので、できればこの設定は有効にしておいた方がいいかもしれません。

Invariant とは…

で、まあ、Ordinal でいいものは Ordinal にするとして。 どうしてもカルチャー依存なものは InvariantCulture にするとして。 次の問題は InvariantCulture が invariant (不変な) という名前を名乗っているくせに北米基準という点。

以下のようなコードを書くと、北米フォーマットになります。

using System.Globalization;
Console.WriteLine(new DateTime(2021, 8, 22).ToString(CultureInfo.InvariantCulture));

90年代の IT 業界にはあるあるだったんですけどね、「北米がデフォルト」な動作。 Java なんかでも、println(new Date()) すると Sun Aug 22 08:18:22 UTC 2021 とかになりますよね、たぶん。

元をたどるとこの「未設定なら北米準拠にする」みたいなの、 「"C" ロケール」とか言われてるみたいですね。 古い C 言語多言語対応ライブラリがそういう挙動だったからという。

一方の2000年代言語だと yyyy-MM-dd フォーマットなことが多いんですが(Go とか Rust とか)…

まあ、日本人にとって困るのはたぶん日付のフォーマットくらいだと思います。 小数点は北米と同じなので。 ところがまあ、小数点・桁区切り記号に . を使うか , を使うかは、結構世界で2分されているみたいなので注意が必要です。

北米フォーマット

日本にいると「欧米」なんていう区切りで一括りにしてしまいがちなんですが… アメリカ合衆国、結構異端ですからね。

英語圏の中でも Arbitrary Retarded Rollercoaster(勝手で遅れたジェットコースター)とか言われてるネタ画像出てきますからね、これ。 ちなみに、「アメリカはおかしい」って主張に対して「違うよ、アメリカの中でも合衆国だけだよ、一緒にしないで」までがセット。

それを見て「さすがに言いすぎじゃない?」って思って調べるとマジで1国だけ浮いているという。 特に日付。 dd-MM-yyyy (リトル エンディアン)と yyyy-MM-dd (ビッグ エンディアン)はどちらも分からなくはないものの、 さすがに MM-dd-yyyy (ミドル エンディアン???)はちょっと…

もちろん元々イギリスの文化なんですけども、当のイギリスはメートル法に改宗済み。 (といっても、法律上メートル法に変わってはいても、人が急に変われるわけもなく街中にはヤードポンドが残ってるそうですが。) 日付はぶれ気味(当人たちも混乱する)なので、Aug 22 みたいに書かないと MM-dd なのか dd-MM なのかわからなくなるから避けるみたいです。

という感じなので、そんな異端児をもって Invariant とか言われましても困ります… という感じになります。

最近、C# 配信をしていて、文字列処理の話をするたびに 「ヤードポンドの国なので」という話になるのはこういう背景から。 実害を受けるのは日付の MM-dd-yyyy 書式だけなんですけども、 MM-dd-yyyy の国 ≒ ヤードポンドの国 ≒ ファーレンハイトの国 という。

ISO 8601 フォーマット

と言うことで最近至った結論として、文字列処理は、

  • Ordinal にできるならそれを、何らかのカルチャーに依存するなら InvariantCulture にする
  • その上で、日付だけはフォーマットを明示的に指定

でやらないと事故る。

そこでまあ、ちょうど C# 10.0 で $"{X}"カスタマイズをパフォーマンスを落とさずにできる仕様が入るので、 それを使って InvariantCulture 指定、かつ、日付だけは O 書式(ISO 8601形式)を常に指定するコードを書いてみたり。

ちょうどカルチャー依存で困る時期だったので、ちょうど C# 10.0 でこの機能が入ったのは助かるかも。

RC 出てた話からの、GA 迫ってたりする話

$
0
0

8月からしばらくブログをさぼっていたわけですが。 ブログとしてはお久しぶりです。

C# 10.0 記事

まあ、とはいえ、別に消息不明になっていたわけでもなく、C# によるプログラミング入門の方の記事書きをちゃんとしていただけでして。

更新履歴

C# 10.0 機能リストのうち、最低限必要なものは埋まったというか、残りは、

  • 限られた人だけが使う(大部分の人は間接的恩恵しか受けない)もの
  • 細かい挙動変更
  • C# 10.0 リリース時点でどの道 <LangVersion>preview</LangVersion> 必須なもの

だけのはずです。

C# 10.0 記事執筆状況

まあこの時期どうせネタ元であるところの csharplangroslyn も11月の .NET 6.0 / C# 10.0 正式リリースに向けた作業をしているわけで、 うちのサイト的にも C# 入門ページを更新する方の優先度高めでおかしくないはず。 (という言い訳。)

RC (リリース候補版)出てた

そうこうしている間に、 .NET 6 も RC になりVisual Studio も RC になり、 それに対するライブ配信では「今日の本題は "go-live" の一言で終わりです。あとはその先の話を」とかやっていたりしました。

GA (正式リリース)する

そして、Visual Studio 2022 も .NET 6.0 も C# 10.0 も、来週には正式リリース(Generally Available)になりますね。 正式リリースされた暁には… 毎年あるあるなんですが、プレビューの時に遊びつくしていてリリースのタイミングで改めて言うこともないので記念雑談でもしようかと…

C# 10.0 / .NET 6.0 / Visual Studio 2022 正式リリース記念

今年、なんか、.NET Conf の前日に Visual Studio 2022 のローンチ イベントをやるみたいですね。 Visual Studio 2022 はそのタイミングで正式リリースとのこと。

ということで、上記「記念配信」は、Visual Studio はリリース済み(アメリカ太平洋標準時8日以降)、 .NET Conf 開催(同 PST 9日8時半)直前のはずの、日本時間の9日夜にする予定です。

その先の話しようか

上記 RC ライブ配信でも「その先の話」してましたし、直近のライブ配信なんて「時代は既に what's next for VS」とかやってたわけでして。

要は、うちのブログで「ピックアップRoslyn」って銘打ってやってる話を最近さぼり気味で、ネタがたまり気味…

C# 10.0 記事の方が(ニッチなネタを除けば)落ち着いたんで、ちょこちょこ消化していければいいなぁと思う所存です。(「明日から本気出す」なやつ。)

【C# 10.0】ファイル スコープ名前空間 (一斉置換設定)

$
0
0

昨日、C# 10.0 が正式リリースされたわけですが、皆様もう C# 10.0 へのアップグレードはお済でしょうか(当日アプグレが当たり前のような口調)。

まあ、 C# はそんなに大きな破壊的変更はしない言語ですし、 TargetFramework や LangVersion を書き換えるだけならそこまで大きな問題は起きないんじゃないかと思います。 (たぶん。 僕は CryptoStream の破壊的変更由来のテスト失敗を1件踏みましたが。)

あえて踏み込むのであれば、

  • 名前空間を namespace N {} からファイル スコープな namespace N; に書き換え
  • ImplicitUsings を true にして未使用 using になる部分をごっそり削除

とかやると、機械的にできてリスクは低いものの大量の差分行を産むコミットを作れたりします。 ASP.NET では95万行の差分が出たそうですよ(もちろん、「Hide whitespace」とかやるとほとんどの行が消える)。

とりあえず今日はこのうち前者、 ファイル スコープ名前空間の一斉全置換の話をしようかと思います。

ファイル スコープ名前空間

C# 10.0 のテーマの1つは「シンプル プログラム」になっています。 その一環で、

namespace Namespace
{
    class A { }
}

という従来コードを、

namespace Namespace;

class A { }

と書き換えられる機能(ファイル スコープ名前空間)が入りました。

まあ、名前空間とかパッケージ宣言とかで1段インデントを下げない言語の方が多いですしね。

C# はこの手の「新しい体験が得られるわけではなく、単にシンプルに書けるようになるだけ」みたいな新文法はこれまでそんなに積極的に追加はしてこなかったんですけども、 最近の流暢に乗ってついに折れた感じはします。

ファイル スコープ名前空間を採用する?

正直まあ、「新しい体験」は何もなく、単なる書式的な問題になるわけで、 既存コードに導入するモチベーションはそんなに高くないと思います。 手動で書き換えろと言われたら絶対に拒否。

ですが、全自動だと言われたら?

既存コードベースは1操作で変換可能で、 新規 C# ソースコードの追加時にもデフォルトでファイル スコープ名前空間な状態のファイルができるとしたら?

自分は「全自動ならまあ、やってもいいかな」くらいには思っています。

設定が必要 (.editorconfig)

ただまあ、この機能、「絶対にやれ」とも「絶対にやるな」とも言いにくい割かしどっちでもいい機能なわけでして。 「使えるならファイル スコープ名前空間を率先して使う」というモードにするためには設定が必要です。

書式・Code Fix 上の問題なので、Visual Studio のオプションで設定するか、 .editorconfig に設定を書くかする必要があります。 どちらを好むかはプロジェクトごとに違うでしょうから、Visual Studio オプション(全部のプロジェクトに影響)ではなく、 .editorconfig で設定する方が好ましいと思います。 (なのでそちらだけ説明します。)

ファイル スコープ名前空間を率先して使いたい方は .editorconfig に以下の行を追加してみてください。

[*.{cs,vb}]
csharp_style_namespace_declarations=file_scoped:suggestion

これで、Visual Studio の一斉置換機能が働くようになります。

Convert to file-scoped namespace

この行だけを修正するか、ドキュメント全体、プロジェクト全体、ソリューション全体を一斉置換するかはお好みでどうぞ。 ソリューション一斉置換とかすると数千~数万行の差分を作れます。 僕は昨日・今日で合計10万行くらいの差分を作りました。

末尾の suggestion (Visual Studio の Error List ペインにメッセージが出ちゃう)のところは silent (何もメッセージは出ないものの Code Fix を掛けることはできる)でも warning (修正しないと警告が出る)でも大丈夫です。

ちなみに、warning にしておけば、dotnet format コマンドでの自動整形も掛かります。 過激派の方はぜひとも warning に。

Viewing all 483 articles
Browse latest View live