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

【C# 10.0】 ImplicitUsings (自動 global using)

$
0
0

今日は先日のファイル スコープ名前空間の話に続いて、 global using に関する話を最初にブログに書いたときにちょこっと「正式リリースまでには変更が掛かる予定」と話していた ImplicitUsings の話をしたいと思います。

global using

何回か話してはいるんですが、 .NET 6 SDK から、C# プロジェクトのテンプレートの初期状態が以下のような(コメントを除けば)1行だけのソースコードになっています。

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

C# コンパイラーとしては global using という文法を追加したわけですが、 わざわざ C# の文法として追加した(コンパイル オプションにはしなかった)のは Source Generator を使って global using を生成するような手法も取れるようにするためです。

ただし、主な用途は上記のような「テンプレートの初期状態を簡素化する」というもので、 多くの場合「自動的に裏のどこかで global using が追加されている」みたいな使われ方になります。

global using の自動生成

で、誰が「自動的に global using を追加」しているかというと、.NET 6 SDK です。 .NET 6 / Visual Studio 2022 で新規プロジェクトを作って、1回 build してみて、 obj/Debug/net6.0 フォルダーを覗いてみると以下のようなファイルが ConsoleApp1.GlobalUsings.g.cs みたいな名前で作られています。

(Debug のところは build 設定次第では Release になりますし、 net6.0 のところは TargetFramework、 ConsoleApp1 のところはプロジェクト名によって変わります。)

global usings

ちなみに、このコードはコンソール アプリの場合の結果で、 プロジェクトのタイプごとに生成される global using 対象の名前空間が変わります。

例えば Windows Forms の場合は System.DrawingSystem.Windows.Forms が、 Web アプリの場合は Microsoft.AspNetCore.*Microsoft.Extensions.*System.Net.Http.Json などの名前空間も追加されます。 この辺りは Windows Forms や Web を作っているチームごとのポリシーによるみたいです。

ImplicitUsings

.NET 6 Preview 7 の頃に書いたブログで、初期案としては「TargetFramework が net6.0 の時は無条件に global using を自動追加」みたいなことをやろうとしていました。

が、global using は既存コードベースに後から追加するとたまに事故ります。 分かりやすい例でいうと、自前で LINQ っぽい拡張メソッドを定義した場合。 例えば .NET 5 の頃に以下のようなコードを書いていたとして、 これを .NET 6 にアップデートしたら、「SelectSystem.LinqMyExtensions の2か所にあって弁別できない」というエラーが起き得ます。 (実際、Preview 7 の頃はエラーになりました。)

using System;
using MyExtensions;

var result = new[] { 1, 2, 3, 4 }.Select(x => x * x);

namespace MyExtensions
{
    static class Enumerable
    {
        public static IEnumerable<T2> Select<T1, T2>(this IEnumerable<T1> array, Func<T1, T2> selector)
        {
            // 実装は省略。
            return null!;
        }
    }
}

わざわざ Select を自作する人は少ないかもしれませんが、 例えば、「これまで標準でなかったから MinByMaxBy を自作していた。これらが .NET 6 で標準入りした」みたいな衝突は割かし起こるんじゃないかと思います。

その他、被りやすい名前でいうと Task クラスがそうで、global using System.Threading.Tasks; の自動追加が問題になったりします。

あと、「TargetFramwork は net6.0 だけど、 LangVersion は 9 とか 8 とかで維持したい」みたいな場合に「global using を生成してしまったけど C# 9.0 以前では使えない」というコンパイル エラーを起こしていました。

ということで、明示的に設定を追加したときだけ「global using の自動追加」が働くように変更されました。 具体的には、csproj に以下の1行(ImplicitUsings オプションが true もしくは enable)があるときにだけ自動追加が働きます。

    <ImplicitUsings>enable</ImplicitUsings>

で、この行は、.NET 6 SDK を使って新規プロジェクトを作成すると、初期状態で入っています。 .NET 6 SDK の、例えばコンソール アプリの csproj の初期状態は以下のような感じ。

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
</Project>

Using オプション

ちなみに、(C# ソースコードで global using を書くのではなくあくまで csproj 設定で) 自動的に global using される名前空間を追加したり削除したりする手段も用意されています。

ItemGroup 配下に Using というタグを書いて、

  • Include 属性を書くと名前空間追加
  • Remove 属性を書くと名前空間削除

になります。

例えば以下のように書くと、System.Text.RegularExpressions 名前空間が追加されて(Regex クラスなどが使える)、 System.Linq 名前空間が削除されます(自前 LINQ との衝突がなくなる)。

<Project Sdk="Microsoft.NET.Sdk">

  他の設定は省略

  <ItemGroup>
    <Using Include="System.Text.RegularExpressions"/>
    <Using Remove="System.Linq"/>
  </ItemGroup>
 
</Project>

全域一括 ImplicitUsings

この ImplicitUsings と、あと、 C# 8.0 / の頃からある Nullable オプションですが、まあ、既存コードベースを壊さないために opt-in (明示的に書かないと有効化されない、デフォルトで無効)なわけです。

とはいえ、今、まっさらな状態から新規コードを書き始めるにはこの2つは「ノイズ」です。 そこでお薦めするのが Directory.Build.props に書き足してしまう手法。

この名前のファイルに csproj に入れたい設定類を書いておくと、 そのファイルがあるフォルダー配下にある全 csproj に対して設定が有効化されます。 なので、リポジトリのルート フォルダーに以下の内容で Directory.Build.props ファイルを置いておけば、リポジトリ全域に対して ImplicitUsingsEnullable が有効化されます。

<Project>
 
  <PropertyGroup>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
</Project>

新規リポジトリにはこのファイルを置いておいていいんじゃないでしょうか。 (ImplicitUsings に関しては既存リポジトリに対しても、前述のような名前の衝突は経験上、数千~数万行に1個くらいしか起きない程度なので割かし追加しちゃっていいと思います。)


TargetFramework net5.0 なコードを .NET 6 ランタイムで動かす

$
0
0

.NET のアップデート

昔の C# アプリ (例えば去年作った TargetFramework net5.0 なアプリ)をそのまま最新のランタイム(例えば .NET 6 ランタイム)で動かすことを考えます。

.NET は API レベルでの破壊的変更はめったにないので、 「API が合わなくてロードできない」みたいな根本的な問題はほぼ起こりません。

一方、挙動レベルでは時々破壊的変更があるんで、確実に動く保証はなかったりします。 (それでも、体感、9割方は動きますが。)

ここ数バージョンであった影響がありそうな変更でいうと、

  • .NET Core 3.0 の頃同期 I/O が例外を出すようになったものがちらほらある
    • ネットワークなどを介する場合、非同期でないとパフォーマンスが出ないので
  • .NET 5 で、国際化対応に ICU を使うようになった
    • 文字列の紹介順や、IndexOf の挙動がちょっと変わった
  • .NET 6 で、FileStream とか CryptoStream の挙動がちょっと変わった

とかがあって、時々、最新のランタイム上で動かそうとしてもうまくいかないことがあります。

アップデート手順

そういうのは「わかってる人だけやって欲しい」ということらしく、 .NET Core 3.1 以降の .NET は「バイナリは昔のままで最新のランタイム上で動かす」という操作に対してかなり保守的です。

例えば、オプション指定なしだと、.NET 5 向けに作った C# アプリを .NET 6 ランタイム上で動かせません。

手間をかけられるなら、

  • プロジェクトの TargetFramwork を net6.0 に上げる
  • SDK/ランタイムも .NET 6 に上げる
  • その状態で単体テストが通るようにコードを書き換える

という3つを同時にやってくれと言うことだと思います。 (実際、挙動の破壊的変更がまれにある以上、そうするしかアップデート後の動作保証は取れないはずですが。)

とはいえ、できることからコツコツやっていきたいことだってよくあるわけで、

  • CI 環境の .NET SDK を先にバージョンアップしておく
  • それに合わせてできるプロジェクトからちょっとずつ TargetFramework を変更していく

みたいなことをしたい方も結構多いんじゃないかと思います。

Roll Forward

まあ、API レベルでの破壊的変更がほとんどなく、挙動レベルでも9割方は動くものに対して常に最近に追従する作業が必要かという話もあり。 ちゃんと、「古いバイナリを最新のランタイムでそのまま動かす」というオプションがあります。

正確に言うと、「バージョン不一致のどき、どのくらいずれてても OK か」を指定するためのオプションがあって、これを roll forward と言います。

デフォルトが Minor で、これがなかなか厳しい… (メジャー バージョンが一致するものがないと実行できない。)

こうことで、先ほどの「.NET SDK を先にバージョンアップしておく」シナリオをやるなら roll forward 設定の変更が必要になります。 Major か LatestMajor なら動かせるはずですが、 僕みたいな「常に最新の SDK/ランタイムに追従」ポリシーの人はとりあえず LatestMajor で大丈夫です。

Roll Forward オプションの指定方法

Roll Forward オプションの指定の仕方はいくつかあるみたいです。 dotnet コマンドに直接オプションを書く方法もありますし、

dotnet run --roll-forward LatestMajor

(ただ、dotnet run はこのオプションを受け付けるものの、dotnet test は受け付けないらしい?)

global.json に書いておくのでもいいそうですし、

{
  "sdk": {
    "version": "6.0.100",
    "rollForward": "latestMajor"
  }
}

環境変数で DOTNET_ROLL_FORWARD を設定しておくのでもいいそうです。

今回のシナリオ(CI)だと、たぶん環境変数を設定するのがよくて、 例えば GitHub Actions の build.yml に以下の行を足せばいいということになります。

env:
  DOTNET_ROLL_FORWARD: latestMajor

実例

昔、GitHub Actions を試してみるためだけに空っぽのライブラリに空っぽの単体テストを定義したリポジトリがあったので、 それを .NET 5 から .NET 6 にアップデートしてみました。

CI が失敗:

CI が成功:

【C# 10.0】 AppendLiteral("")

$
0
0

C# 10.0 で、文字列補間に対するパフォーマンス改善が入りました。 例えば、以下のようなコードがあったとして、

static string A(int x, int y) => $"({x}, {y})";
static string B(int a, int b, int c) => $"{a}.{b}.{c}";

C# 10.0 では $"" の部分がそれぞれ以下のように展開されます。

using System.Runtime.CompilerServices;

static string A(int x, int y)
{
    DefaultInterpolatedStringHandler h = new(4, 2);
    h.AppendLiteral("(");
    h.AppendFormatted(x);
    h.AppendLiteral(", ");
    h.AppendFormatted(y);
    h.AppendLiteral(")");
    return h.ToStringAndClear();
}

static string B(int a, int b, int c)
{
    DefaultInterpolatedStringHandler h = new(4, 2);
    h.AppendFormatted(a);
    h.AppendLiteral(".");
    h.AppendFormatted(b);
    h.AppendLiteral(".");
    h.AppendFormatted(c);
    return h.ToStringAndClear();
}

今日の話はこの AppendLiteral のところの最適化の話。

インライン展開

上記の展開結果を最初に見た時の感想は「AppendLiteral(char) はなくていいの?」でした。 C# 的に、文字 ('.') は単なる数値(2バイトの値型)なのに対して、文字列(".") は参照型(ヒープ アロケーションが掛かる)なので、効率が悪いんじゃないかと。

実際、例えば類似のメソッドとして、StringBuilder.Append なんかは「文字列じゃなくて文字のオーバーロードを使え」というコード解析が出てきたりします。

文字列じゃなくて文字のオーバーロードを使え

何も対処しないと確かに問題になるっぽいんですが、これに対して、DefaultInterpolatedStringHandler.AppendLiteral の実装を工夫して、効率を落とさないようにしているそうです。

今現在(2021/11/7)の DefaultInterpolatedStringHandler.AppendLiteral の中身は以下のような感じ。

DefaultInterpolatedStringHandler.cs#L136

まんまコメントが書かれていますが、

AppendLiteral is expected to always be called by compiler-generated code with a literal string. By inlining it, the method body is exposed to the constant length of that literal, allowing the JIT to prune away the irrelevant cases. This effectively enables multiple implementations of AppendLiteral, special-cased on and optimized for the literal's length. We special-case lengths 1 and 2 because they're very common, e.g.

1: ' ', '.', '-', '\t', etc.
2: ", ", "0x", "=>", ": ", etc.

but we refrain from adding more because, in the rare case where AppendLiteral is called with a non-literal, there is a lot of code here to be inlined.

文字列長が1文字と2文字のときの特殊分岐を書いた上で、 AggressiveInlining を付けています。 要点だけを抜き出すと以下のようなコード。

using System.Runtime.CompilerServices;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void AppendLiteral(string value)
{
    if (value.Length == 1)
    {
        // value[0] しか参照しないコード
        return;
    }

    if (value.Length == 2)
    {
        // value[0], value[1] しか参照しないコード
        return;
    }

    // 汎用ロジック
    AppendStringDirect(value);
}

AppendLiteral には文字通りリテラルしか渡ってこないという前提ありきですが、 これで1文字の場合と2文字の場合はかなり速くなるとのこと。

JIT 時最適化で、 1文字の文字列リテラルが渡ってきたときには if (value.Length) == 1)、 2文字のが渡ってきたときには if (value.Length) == 2) の中身しか残らないそうです。

【C# 10.0】 トップ レベル ステートメントの変更点

$
0
0

そういえば、文法的な変更ではないのでどこにも告知は出ていないもの(サイレント修正)なんですが、トップ レベル ステートメント (C# 9.0 で追加)に変更点が2つあります。

空ステートメント禁止

以下の2つのコードを見比べてください。

1つ目:

class Program
{
    static void Main() => Console.WriteLine("Hello World!");
}

2つ目:

;
class Program
{
    static void Main() => Console.WriteLine("Hello World!");
}

C# 9.0 当初、2つ目のコードもコンパイルできていました。 そして、実行結果がどうなるかと言うと…

  • 1つ目: Program.Main が呼ばれて、Hello World! が表示される
  • 2つ目: トップ レベル ステートメント扱いされて、何も表示されない

さすがに ; 1個で挙動が変わっちゃうのはためらわれるというか、 「2つ目で何も表示されなくなるのはバグだ」と認識しちゃう人もいたので修正がかかりました

今は、2つ目のコードはコンパイル エラーになります。 空ステートメント1個だけのトップ レベル ステートメントは禁止。

でも、以下のようなコードは今 (C# 10.0) でも認められてるんですよね…

空じゃないステートメントもある:

;
Console.WriteLine();

空ブロック:

{}

; だけのものが禁止された経緯を考えると、{} だけのものも禁止されてもおかしくはないんですけど。 「Main を呼ぶかトップ レベル ステートメントを呼ぶか」の分岐条件が緩すぎるんですよね。 今後どうなるか…

トップ レベル ステートメントを使った時のクラス名

トップ レベル ステートメントを使った時、例えば以下のようなコードを書くと、

Console.WriteLine();

扱いとしては以下のようなコードに展開されていました。

using System;

internal class <Program>$
{
    private static void <Main>$(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

クラス名、メソッド名がどうなるかは仕様には明記されておらず、実装依存(変更が掛かっても文句は言えない)です。 とりあえず、「通常の C# では書けない名前」(unspeakable name というそうです)になっていました。 実装依存ですが、現在の実装では <Program>$ みたいに <>$ を入れて unspeakable にしています。

ところが、クラス名が unspeakable だと、ASP.NET の単体テストで困ったそうです。 ということで、クラス名だけは speakable な Program に変更。 C# 10.0 では上記のコードは以下のような展開結果に変更されています。

using System;

internal class Program
{
    private static void <Main>$(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

トップ レベル ステートメントだけを使っている分には特に影響のない修正のはずなんですが… 例えば以下のようなコードがコンパイル エラーを起こすようになります。

Console.WriteLine("Hello World!");

internal class Program
{
}

コンパイラーが生成する Program クラスと、コード中に手書きした Program クラスが衝突しています。

一方で、現状のこの実装を逆手に取ると、以下のようなコードはコンパイルできるようになります。

A(); // Program.A が呼ばれる。

partial class Program
{
    public static void A() => Console.WriteLine("Hello World!");
}

ただ、Program というクラス名が仕様書上に明記されているわけではないので、将来もこのコードが有効であるという保証はあんまりできません。その点はご注意ください。

Unicode 演算子 (∑Σ とか ∫ʃ とか)

$
0
0

C# ライブ配信をしていて、「括弧用の記号の種類が少なすぎる」みたいな話題から、 「あるよ、括弧。Unicode には」みたいな話になり、 「Swift ではマジでいろんな記号が使える」という話に脱線したときの話。

配信では「括弧がたくさんある」という話と「Swift では演算子にいろんな文字が使える」という話が混ざっていて、 実際に Swift で色々使えるのは括弧ではないんですけど、演算子の方は本当に Swift で使えるものがかなり自由が効く仕様になっていまして。 例えば以下のコードはコンパイルして実行できます。 (1から5の和で、15が出力されます。)

prefix operator 

prefix func  (x: [Int]) -> Int {
  var sum = 0
  for i in x {
    sum += i
  }
  return sum
}

let Σ = [ 1, 2, 3, 4, 5 ]

print(Σ)

∑Σ

シグマシグマ。

ちなみにこれ、同じ記号に見えて実は違う文字でして。

  • (1個目の)∑ : U+2211。数学記号の「和」の意味のシグマ。
  • (2個目の)Σ : U+03A3。ギリシャ文字大文字のシグマ。

Swift は、

という仕様になっているので、上記のようなコードが合法になります。

シグマとシグマ

まあ、「なんで文字コード分けた」という話ではあります。 Unicode、同じような意味、読み、形の文字は統合する方針じゃないの? シグマとシグマを分けるんだったら、日中韓漢字統合(いわゆる中華フォント問題)とかやめてよ…

そしてまあ、同じようなことができる組み合わせは他にもいろいろとあります。 ぱっと思いつくのは以下のようなもの。

パイ:

  • ∏ : U+220F。数学記号の「積」の意味のパイ。
  • Π : U+03A0。ギリシャ文字大文字のパイ。

long S:

  • ∫ : U+222B。積分記号。積分記号自体が「S を引き延ばしたもの」が由来。
  • ʃ : U+0283。発音記号の "esh"。S 亜種の発音。
    • letter 扱いらしく、変数名に使える。

Union:

  • ∪: U+222A。Union (和集合)の数学記号。
  • U: U+0055。ローマ字の(ASCII コード中にある) U の大文字。

c の丸囲み:

  • © : U+00A9。コピーライト記号。演算子にできるらしい…
  • c⃝: U+0063 U+20DD。c に combining enclosing circle を付けて丸囲み。
    • サポートしているフォントは少ないものの、ものによっては © と似たような描画をされるはず。
    • letter + diacritical mark なので変数名に使える。

他に ℵ(U+2135。ヘブライ文字を元にした数学記号のアレフ)と א (U+05D0。元々のヘブライ文字のアレフ)とかも思いついたんですけど、これは両方後も letter (変数名に使えて、演算子には使えない文字)でした。 ちなみにこのアレフに至っては、数学記号の方は L to R (左書き)、ヘブライ文字の方は R to L (右書き)の文字で、後ろに続く文字のレンダリングに影響を及ぼしたりします。

Regional Indicator (国旗絵文字)

$
0
0

今日は10/31 にやった配信で出てた国旗絵文字の話とか

絵文字を検索したら別の絵文字が引っかかるというのの原理的な話になります。

絵文字を検索したら別の絵文字が引っかかる

元ネタ

まず配信中になんで国旗の話が出たか(「先日、国旗絵文字をどうデコードするか問題を見たなぁ」というのの元ネタ)の紹介。

この配信の数日前に、こんなネタがバズっておりまして。

ここから派生して、国旗絵文字の仕様がいかにひどいかという話に…

UTF-8 の勝利

UTF-8 「多バイト文字の1バイト目」と「多バイト文字の2バイト目以降」が被らないように作ってあります。 その結果、任意のバイト列の任意の区間を切り出したとしても、以下の3つのうちのいずれかであることが確実に判定できます。

  • 前後に何がつながろうと関係なく、一意にデコードできる
  • 前後数バイトを読まないと正しくデコードできないことがわかる
  • 前後に何がつながろうと関係なく、不正な(デコードできない)バイト列だとわかる

上記の「『ここ』の中に『海』が見つかる」みたいな、「一部区間を切り取ると別の文字に見える」みたいな問題を絶対に起こしません。 (ただしこれは符号点レベルでの話。「国旗絵文字問題」みたいな、書記素って単位で区切る必要があるものに関しては UTF-8 でも今だ問題あり。後述。)

UTF-16

UTF-16 も「2バイトずつ区切る」という前提を持つ限りには同様に、「一部区間を切り取ると別の文字に見える」みたいな問題を絶対に起こしません。

UTF-16 には4バイト使って表現する文字があるんですが、 それもハイ サロゲート(上位2バイト)とロー サロゲート(下位2バイト)と呼ばれる文字コードが完全に分かれていて、順序が違ったり、ペアになってない文字は「不正な UTF-16 データ列である」という判定ができます。一部区間を切り取ったとしても別の文字と誤認識することはありません。

ただし、これはあくまで「2バイトずつ区切る」という前提の話で、 「1バイト削る」とかやってしまうと誤判定します。 そういう意味では UTF-16 は Shift_JIS とか EUC-JP とかの時代からそんなに変わっていないです。 (この辺りも UTF-8 が主流になった理由の1つ。)

分かりやすいのは以下のような文字列。この例だと ‰ (パーミル記号)が〠 (顔郵便マーク)に化けています。

using System.Text;

// UTF-16 (Little Endian) だと…
var s1 = "‰‰"; // 30 20 30 20
var b = Encoding.Unicode.GetBytes(s1);
var s2 = Encoding.Unicode.GetString(b[1..^1]); // 20 30
Console.WriteLine(s2); // 〠 (U+3020)

他にも、2バイト目に ASCII 文字を含む文字なんかも地雷です。 2バイト目(UTF-16 Little Endian だとすると上位バイト)が 5C (ASCII だと \ 記号)の文字とかはなかなかやばい地雷を踏めます。 C 言語みたいに \ 記号に特別な意味がある言語がありますんで。

割と使いそうな文字だと 尺、尼、尾、局、居、届 とかですかね。 例えば「局」は U+5C40 なんですが、これを UTF-16 LE で保存してから ASCII とか UTF-8 で読み込みなおすと @\ になります。

C 言語だと、行末に \ を置くと「改行コードを無視して次の行とつなぐ」みたいに意味になるので、 //局 みたいなコメントを書いて、UTF-16 で保存して、文字コード指定なし(今時だいたいのコンパイラーで UTF-8 扱い)でコンパイルするとコメントの後ろの行が消えます。

#include <stdio.h>

void main()
{//局
    printf("Hello World"); // なぜか表示されない
}

UTF-8 の逆転敗北(主に絵文字のせい)

「コンピューター内部における文字ってなんだ」という話になってくるんですが、結局、Unicode には「複数の文字を組み合わせて1文字を表現する」みたいな仕様があったりします。

前述の「符号点レベルでは大丈夫だけど、書記素のレベルではダメ」という話なんですが、

  • 符号点 (code point): 32ビット数値を振られている文字
  • 書記素 (grapheme): レンダリング上1文字に見える単位。複数の符号点から成り立つことがある

みたいなのがあります。

例えば以下のような「文字」(書記素)。

  • 人 + 職業
    • 👩‍💻: 👩 (U+1F469), U+200D, 💻 (U+1F4BB)
    • 👩‍⚕️: 👩 (U+1F469), U+200D, ⚕ (U+2695), U+FE0F
    • 👩‍🎓: 👩 (U+1F469), U+200D, 🎓 (U+1F393)
    • 👩‍🏫: 👩 (U+1F469), U+200D, 🏫 (U+1F3EB)
  • 職業 + 性別
    • 👮‍♀️: 👮 (U+1F46E), U+200D, ♀ (U+2640), U+FE0F
    • 🕵️‍♀️: 🕵 (U+1F575), U+FE0F, U+200D, ♀ (U+2640), U+FE0F
    • 💂‍♀️: 💂 (U+1F482), U+200D, ♀ (U+2640), U+FE0F
    • 👷‍♀️: 👷 (U+1F477), U+200D, ♀ (U+2640), U+FE0F

まあ、理由は分かりますよね… この例の場合はジェンダー問題。 この他に、肌の色とか髪の色とかのバリエーションもあります。

「亜種のために符号点は増やさない」というのが現在の絵文字の方針っぽい雰囲気でして、 最近(Unicode 13.0)だと「色違いの動物」絵文字とかも「符号点を複数組み合わせた書記素」として追加されました。 (Unicode 13.0 (2020年リリース)は Windows 10 だと表示できないので注意。Android, iOS, Windows 11 では表示できます。)

  • 色違い亜種の動物
    • 🐈‍⬛: 🐈 (U+1F408), U+200D, ⬛ (U+2B1B) の3符号点
    • 🐻‍❄️: 🐻 (U+1F43B), U+200D, ❄ (U+2744),️ U+FE0F の4符号点

で、「文字の中に別の文字が現れる」問題が再発します。

それで冒頭のスクショになります。再掲:

絵文字を検索したら別の絵文字が引っかかる

👩‍💻👩‍⚕️👩‍🎓👩‍🏫👩‍⚖️👩‍🌾👩‍🍳👩‍🔧 という絵文字列の中に 👩 という別の絵文字が見つかってしまいます。

(ほんとはそれは「正しく書記素を扱えていない」ということになるのでエディターの不具合なんですが。 後述しますが、書記素単位で検索するというのが必ずしもいいことではないのでなんとも。)

書記素分割の仕様

符号点は単純に UTF-8 とか UTF-16 をデコードしたら得られる32ビット数値なので非常にわかりやすいんですが。 書記素の方はどうなっているかと言うと、結構複雑な仕様があります。

この仕様の中に「ここで文字を区切ってはいけない」とか「ここで文字を区切る」みたいなルールが書かれています。 このうち、絵文字の分割は「3 Grapheme Cluster Boundaries」(書記素クラスターの境界)の仕様に従います。

この「区切ってはいけない」のルールに従うのではあれば、 「👩‍💻 の中に 👩 を見つけてしまう」みたいなことも避けられます。 U+200D (Zero Width Joiner、2つの符号点をくっつけるための文字)の前後は区切ってはいけないというルールがあるので、👩‍💻 (U+1F469, U+200D, U+1F4BB) を 👩 (U+1F469)と 💻 (U+1F4BB)に分けてはいけないということになります。

改行も書記素

ちなみにまあ、絵文字以外にも書記素になるものはあって、 日本人でも注意が必要なものとしては「CR と LF の間は区切ってはいけない」(GB3)というルールがあります。

歴史的背景から、Windows での改行コードは CR LF (U+000D, U+000A)で、 Unix 系 OS の改行コードは LF (U+000A) なわけですが。 これを横着して、「LF で検索すれば CR LF でも LF でも引っかかるはず」と思っていると事故る(CR LF にマッチしない)可能性があるということです。 書記素として見るなら CR LF と LF は別の文字ということになります。

「まあ普通『書記素として検索』とかしませんよね!」 とか思っていたら、C# でも1回事故ってるんですよね。

.NET Core 3.0 から .NET 5.0 にアップグレードしたら IndexOfContains で改行文字の扱いが変わってしまったという。 (ちなみにこれ、カルチャー依存問題です。 さらに言うと結局、.NET 6.0 で改行の扱いが .NET Core 3.0 と同じに戻ったみたいです。)

国旗 (Regional Indicator)

で、そんな元からつらい絵文字の中でも、国旗絵文字の仕様は特にひどいんですよね…

「国コードに相当する2文字を並べて国旗を表現」とかやります。 このために使う文字を Regional Indicator といって、U+1F1E6~1F1FF の範囲に並んでいます。

そりゃね… KR (韓国)と US (アメリカ)を並べたら RU (ロシア)が出てきますとも…

UAX29 の分割アルゴリズム的にはどう対処しているかと言うと…

奇数個の Region Indicator では区切るな。

はい。個数依存です。 先ほどの 👩‍💻 なら U+200D の周りだけ見れば判定可能なんですが。 Regional Indicator の場合は国旗が連続しているとき、端っこまでさかのぼった上で、通算の個数を数えないと判定できません。

AZ (アゼルバイジャン)と ZA (南アフリカ)みたいな国コードもあるので、同じ国の国旗を大量に並べるだけで、2国のうちどちらなのか判定するのが急に大変になります。

(極端な話、1,000個くらい同じ国旗を並べたら、8,000バイトくらいさかのぼらないと AZ なのか ZA なのかが確定しない場合があります。 完全に冒頭の「EUC-JP における『こ』と『海』」と同じ問題を踏んでいて、 「国旗絵文字は教訓を生かしていない」と言われても仕方がない状態。)

「さかのぼる」とか「繰り返す」みたいな処理は他の書記素分割アルゴリズムでも出てくるんですが、「個数を数える」は本当に Region Indicator だけ。

国旗 (Tag Sequence)

「国」(country)というと角が立つんで本当は「地域」(territory)とすべきなんですが、 まあ国旗(地域の旗)絵文字はその後、3つほど追加されています。 EnglandScotlandWales。 (要するに、連合王国内の country を「国」と訳してしまうと国旗になってしまう3地域。)

さすがに Regional Indicator のクソ仕様は反省しているみたいで、新しい旗は別の構成方法を取っています。

例えば England 旗は U+1F3F4, U+E0067, U+E0062, U+E0065, U+E006E, U+E0067, U+E007F という並びになっていて、

  • U+1F3F4: 元々ある「黒い旗」絵文字🏴。
  • U+E0000~E007F: 0~7F の ASCII 文字に対応する「タグ文字」と呼ばれる文字
  • 地域の旗は「黒い旗」から始めて、地域コード(gbeng, gbsct, gbwls)に相当するタグ文字を並べ、最後にエスケープ タグ(U+E007F)で閉じる

という仕様。

開始文字と終端文字が決まっているので、Region Indicator 時代にあったみたいな「連続していると最初まで延々とさかのぼらないと確定しない」問題はなくなっています。

(他に、原理的には、2文字の制限がなくなったのでかなり細かい単位の地域の旗であっても対応できるとか、開始文字を変えることで旗以外の地域に関連した何かを表現するとかできるので、将来の拡張性も非常に高いです。 この仕様に対応していない文字レンダリング環境では単に黒旗🏴が表示されますし、その意味でも親切設計。)

こういう仕様を Tag Sequence って言うみたいです。 無茶苦茶変な仕様ですし、1文字辺りのバイト数もすごいこと(上記の3つの旗はいずれも UTF-8 で28バイト)になるんですが、まあ Region Indicator の反省を踏まえた結果こうなっています。

【C# 10.0 関連】引数なしコンストラクターの Activator バグ

$
0
0

そういえばライブ配信(8月)とか Twitter では話しているものの、ちゃんとこのサイト内には書いていなかったなと言う話。

C# 10.0 で構造体の引数なしコンストラクターが書けるようになりました。

struct A
{
    public int X;
    public A() => X = 1; // ←要はこういうの
}

今年2月にブログで書いてるんですが、これ、C# 6.0 の時に一度採用しようとしたものの、Activator.CreateInstance にバグがあって、いくつかの場面でまっとうに動かないということで延期されたという経緯があります。

で、それは直したし、直っていない頃の古いランタイムはサポート外にしていいだろうということで、晴れて C# 10.0 で採用されました。

ところが、「バグを直したと思ったら別のバグが残ってるランタイムが現存している」と言うことが後から発覚… 実は、 .NET Framework で実行すると引数なしコンストラクターがちゃんと動かいことがあったりします。

旧バグ

C# の構造体は new T()default(T) が同じ「0初期化」を表してた時期が長かったので、Activator.CreateInstance がコンストラクターを呼んでくれず、単に0初期化した値(要するに default(T))を返してくるというものでした。

Activator.CreateInstance を直接呼ぶことはまああんまりないでしょうが、ジェネリック型の new() 制約は内部的に Activator.CreateInstance を使っていて、間接的に影響を受ける人は結構多いと思います。

例えば、先ほどの、引数なしコンストラクターで X を 1 に初期化しているはずの構造体 A を使って以下のようなコードを書いたとします。

var a = New<A>();

// 古いランタイムだとこれで a.X == 0 に
// 1 になるはずなのに…
Console.WriteLine(a.X);

static T New<T>()
    where T : new()
    => new T();

直接 new A() すればちゃんと X が 1 に初期化されるんですが、 New<T> メソッドを介すると 0 になっていました。

C# 6.0 当時の話なので確か、 「.NET Framework 4.5 で問題が発覚して、4.6 では直した(つもり)」 とかだったと思います。 当時のポリシーだと古いランタイムのサポートを切れないのでお蔵入り。

現バグ

直したつもり

実際、.NET Core では(.NET 5, .NET 6 も)ちゃんと直っています。 問題は .NET Framework の方でして、現行の最新版である .NET Framework 4.8 で別のバグり方をしています。

CreateInstance を呼ぶのが1回目なら正しく引数なしコンストラクターが呼ばれるんですが、 2回目以降は default(T) を返してしまうという内容。

察しは付くと思いますが、キャッシュ関連のバグです。 何らかのキャッシュを持たせて CreateInstance を高速化する最適化は後から追加されたものなので、「1回直したはずのものが再発」という状態です。

先ほどと同じ構造体 ANew<T> メソッドを使った場合、 .NET Framework 4.8 で実行すると「2度目がおかしい」という状態になります。

Console.WriteLine(New<A>().X); // 1回目は大丈夫。ちゃんと 1。
Console.WriteLine(New<A>().X); // 2回目以降なぜか 0 に… (.NET Framework 限定のバグ)

TargetFramework net4.8 じゃなくてもバグる

これ、コンパイル時(C# コンパイラー側)の問題ではなくて、 実行時(.NET Framework ランタイム側)の問題なので、 例えばの話、

  1. netstandard2.0 なライブラリで以下のようなコードを書く (LangVersion 指定で明示的に C# のバージョンを 10.0 に上げる)
namespace ClassLibrary1;

public struct A
{
    public int X;
    public A() => X = 1;
}
  1. 以下のようなアプリ コードを書く (これは C# 7.3 でも動く)
using System;

class Program
{
    static void Main()
    {
        // ジェネリックな new() は、内部的には CreateInstance<T>() と一緒
        Console.WriteLine(New<ClassLibrary1.A>().X);
        Console.WriteLine(New<ClassLibrary1.A>().X);
        Console.WriteLine(New<ClassLibrary1.A>().X);
        Console.WriteLine(New<ClassLibrary1.A>().X);
    }

    static T New<T>()
        where T : new()
        => new T();
}

とやると、アプリ側、 netcoreapp1.0 とか net5.0 とかで動かす分には問題なく 1, 1, 1, 1 という結果になるんですが、 これを .NET Framework 4.8 で実行すると、1, 0, 0, 0 という結果になります。

影響範囲

現在の C# は「TargetFramework に応じて言語バージョンを自動選択」という方針になっていて、 「古いランタイムで最新の C# 構文を使う」というのは「わかってる人だけがやってくれ」と言うことになっています。

通常 .NET Framework 4.8 で使える C# は C# 7.3 で、 C# 10.0 の新機能が正しく動かなくても概ね問題は起こさないはず…

なんですが、そこで問題になるのが、 先ほどの「ライブラリ側で C# 10.0 にして引数なしコンストラクターを使っている」という場合。

  1. ライブラリ側が netstandard2.0 で、引数なしコンストラクターを使っている
  2. アプリ側が net48 で、new() 制約越しにジェネリックなメソッドで new T() している
  3. それを Windows 上で .NET Framework 4.8 で実行する

みたいな状況で問題を起こします。

レアな条件ではあるんですが、 「わかっている人が書いたライブラリを、わかっていない人が参照する」というものなので、 「ありえなくもない」くらいには警戒が必要です。

おまけ: Unity

ちなみに、TargetFramework が net48 であっても、 Unity (Mono)で実行する分には問題を起こしません。 あくまでランタイムが .NET Framework の時にだけ起こります。

ベンゼン環の文字コード: ⌬ (U+232C), ⏣ (U+23E3)

$
0
0

ベンゼン環が髪についてる子の Twitter 凍結が解けた記念。

というわけではないんですけども、C# 配信でたびたびネタにしてる Unicode のベンゼン環記号の話。

( 開始10分で Twitter 凍結。ものの数分で数万単位でフォロワーが増えるとか言う不自然な動きが何の不正もなく達成されてしまうのが大手企業勢 VTuber の恐ろしいところ…)

ベンゼン環文字コード

なぜか Unicode にはベンゼン環に文字コードが割当たっています。

  • ⌬ (U+232C)
  • ⏣ (U+23E3)

なぜか。

マジで、「なぜか」。 しかも2文字あります。

うちの配信でなんでよく出てくるかと言うと、2点変な点があるからでして。

  • そもそもなんで Unicode に入ってるのかわからない
  • 2文字ある

文字なの??

Unicode にはまあ、変な文字もそこそこたくさんあるんですが。 概ね、変なやつは「出どころが変」。 要するに過去にそういう文字を入れちゃった誰か(Shift_JIS が犯人である率高め)がいて、それとの互換性のために入っています。 分かりやすい例でいうと ♨ (U+2668、温泉マーク)とかですが、これは Shift_JIS の頃からある文字です。

ところが、ベンゼン環の ⌬ が入ったのは Unicode から。 なんなら、Unicode 1.1 からの追加(要するに最初からはいない)です。

本当に変なんですよね、これ。 Unicode に収録されるにあたって1つの基準になるのが、 「現実にある文献(の本文中)で使われている」というのがあります。 要するに電子化以前からあるあらゆる文字を電子的に表したいという目論見。 「本文中」と注釈してるのは、要するに「図表は除く」という意味。

そこで改めて ⌬ という記号について考えてみた時、 「本文中に書く?そりゃ図表として化学式には出てくるけども…」 ということになります。

本当になんで文字コードが割当たってるのかわからない…

Unicode 1.1 の頃のドキュメントってなかなかネットで見つからないので詳しくは僕も知らないんですが、どうも「科学のシンボル的に使うことがある」みたいな感じで入ったみたいです。

使う?図じゃなく文字として?… 絵文字が文字として普及した今となってはこれも文字かも?とは思わなくもないですが、Unicode 1.1 の頃に? そりゃ ♨ よりはまともかもしれませんけど、わざわざ追加で?

(Unicode で増えた文字を Shift_JIS に逆輸入することがあったりもするんですが、上記のような背景からベンゼン環の ⌬ に関しては徹底抵抗があったらしく、無事、逆輸入は阻止されたそうです。)

2文字ある

さて、ちょこっと化学の話。

ベンゼンは分子式 C6H6 で、炭素 C が六角形につながった有機化合物です。 この辺りは高校の授業とかでも出てくるので多くの方が知っているかと思います。

で、量子力学が発達して分子中の原子核や電子の配置が具体的に予測できるようになる以前、ベンゼンの C は「1重結合が3個、2重結合が3個で結びついている」と思われていました。それを表したのが ⌬ という記号。

その後、分子中の原子の配置が観測できるようになったり、 量子力学を使って電子軌道を計算できるようになると、 どうもベンゼン中の C は正六角形になっていて、1重・2重の結合の区別はないらしいということがわかってきます。 6個の C の間で6個の電子を共有しているようなモデルの方が正確とされていて、それを表現するのが ⏣ という記号を使うようになった背景。

よく言われるのが、「⌬ という記号は間違った理解を助長してしまうので使うべきではない」という話。 記号通りに ⌬ を解釈するのであれば、シクロヘキサトリエンというベンゼンとは違う化学物質になるんですが、ベンゼンと比べて著しく不安定なためもし作れたとしてもすぐに壊れると思われます。

文字追加

なぜか ⌬ という記号を追加してしまった Unicode ですが、その後当然こんな話が出ます、「⌬ は間違っている。⏣ に変更すべき。」と。

ところが、まあ、簡単に「変更」とはできなかったわけです。 Unicode 5.0 の頃の提案によれば、

  • ⏣ の方がモダンで、多くの人が ⏣ を使うようになっている
  • ⏣ の方が実際の物理構造をよく表している
  • ⌬ と ⏣ は同じ分子を表しているものの、意図的に使い分けられることがある(上記のシクロヘキサトリエンみたいな)
  • したがって、バリエーションとしてではなく、別の文字とすべき

はい。その結果、ベンゼン環が2文字になりました。 ただでさえ使われない文字に文字コード2つ目が発生。

実際どうなんですかね。 少なくとも僕は、僕みたいな人間がネタにするか、批判するか以外の場所でこの文字を見たことがないんですが…

Windows でも Android でも iOS でも IME にこの記号出てきませんし。 ちなみにこのブログはググって出て来た文字をコピペで書いています。 「232c」って打って F5 キーを押すことで変換はできるんですが(Windows 10 以降、文字コードから文字を出せる)、それもしてません。コピペです。

意匠としてのベンゼン

ちなみにおまけ。 冒頭で博衣こよりさんをネタにしてしまった手前。

こよりさんの配信背景中に以下のようなロゴがあったり。

博衣こより配信背景画像

「あー、確かに、意匠としては2重結合の方が可愛いもんなぁ」、 「⏣ だとナットか何かに見えるもんなぁ」という気持ちに。

ちなみにこのロゴ、よく見るとシクロヘキサトリエンとしても2重結合の位置がおかしくて、これだと「1つの C に対して結合の腕が5本ある」ということでちょっとだけネットがざわついたみたいです。

いや、まあ、そこはデザインだから…


【C# 11 候補】 {} 中の改行

$
0
0

今日は「実は Visual Studio 17.1 Preview 1 (先月) の時点で既に入ってた」という機能の話。

C# 11 で、$"{ここ}" みたいな「補完穴」(interpolation hole: 補完文字列の {} の中)の改行に関する仕様がちょっと変わります。

文字列リテラル中の改行

C# の文字列リテラルは、@ を付けると逐語的(\ を使ったエスケープをしなくなる)になって、その中には改行を直接入れることができます。

// @ を付けると文字列内での改行 OK になる。

var s1 = ""; // 改行入れれない。
var s2 = @"
"; // 改行 OK。
var s3 = "
"; // 当然これはコンパイル エラー。

この仕様、補間文字列に対しても同様です。

// @ を付けると文字列内での改行 OK になるのは $"" でも一緒。

var x = 123;

var s1 = $"{x}"; // 改行入れれない。
var s2 = @$"
{x}
"; // 改行 OK。
var s3 = $"{x}
"; // 当然これはコンパイル エラー。

補間穴中の改行

C# はほぼ全ての構文で改行の有無を問わないので、例えば以下の2つのコードは全く同じ意味になります。

var x = 123 + 987;
var
    x
    =
    123
    +
    987
    ;

で、補間穴 ({})の中は普通の C# 構文になります。 前述のような「改行の有無を問わない」という常識に照らし合わせると、 以下のようなコードを書けていいはずです。 (C# 10 まではなぜかダメ。)

// なぜかダメだったコード。

var x = 123;

var s1 = $"{
    x
    }";

ちなみに、これに @ を付けると C# 10 でもコンパイルできます。 というか、さらに言うと割かし何でも書けます。 // コメントすら書けます。

// @ を付ければなぜか OK。

var x = 123;

var s1 = $@"{
    x
    +
    987 // コメントすら OK
    }";

C# 11 での変更

で、まあ、$"{}"$@"{}" で挙動が違うの、 仕様的にもそうなってるらしいんですが、 中の人曰く「改行を禁止した実際の理由、覚えてない」とのこと。

挙動が違うのも変なのでさらっと直したみたいです。 気づいたタイミング的に C# 10 正式リリースには間に合わなかったものの、 ほぼ修正は終わってたみたいで、即座に merge、実は 17.1 Preview 1 には入っていたみたいです。

ということで、実は LangVersion preview を入れればもう動くらしい。

LangVersion preview を入れればもう動くらしい

(このスクショは Visual Studio 17.1.0 Preview 1.1 で撮影。)

さよなら、LangVersion default。おかえり、preview (1年ぶり2度目)。

ということで、以下のようなコード、C# 11 候補になっていて、 preview 指定すると現在でもコンパイルできたりします。

// C# 11 候補。

var x = 123;

var s1 = $"こっちは C# 11 から OK {
    x
    +
    987 // コメントすら OK
    }";

var s2 = $@"こっちは元から OK
{
    x
    +
    987 // コメントすら OK
    }
def";

と言うのを昨日の Pull Request を見て初めて気づいたという話でした。

【近々警告追加】小文字 a~z 型名

$
0
0

型名のコードスタイル

C# も最近はコードスタイル的なものに対するおせっかいをするようになってきました。 そのうちの1つが「型名は小文字始まりやめろ」。

型名は小文字始まりやめろ

現在はこれが Suggestion レベルのメッセージ(警告ほど深刻ではないものの、Visual Studio 上で常に下線が表示されて結構「直せ」圧強め)になります。

まあ、このスタイルに違反する人はそんなにいないんじゃないかと思います。 C# の型名と言えば大体大文字始まりの UpperCamelCase。 類縁なプログラミング言語の Java でも型名は UpperCamel なのでこれに関して宗教論争になることはほとんどないかと思われます。

型名とキーワードの弁別

C# は後方互換性をものすごく大事にする言語で、 極端な話20年近く前の C# 1.0 時代に書いたコードも8~9割方そのまま今の C# 10.0 で動くんじゃないかと思います。

そんな言語なので、新文法のためにキーワードを追加したい場合、 ほとんどは「文脈キーワード」(contextual keyword)です。 文字通り「文脈を見てキーワードなのか、通常の識別子なのかを弁別」みたいなことをしています。 この辺り、今年の2月にも同じような話をしていますが、var なんかは

  • var と言う名前の型がどこかに存在したら型名扱い
  • それがなければキーワード

みたいな扱いになります。

で、その2月のブログでも言っていますが、 あんまりにも文脈に頼った弁別をするのはなかなかに大変と言う議題が上がっています。

特に、C# 9.0 でレコード型を追加するにあたって問題になったんですが、 (識別子の中でも特に)型名がキーワードとぶつかると弁別がかなりしんどいそうです。

ということで、record キーワードの追加にあっては破壊的変更(過去に record という名前の型を作って運用しているコードは C# 9.0 にするとコンパイルが通らなくなる)を認めることになりました。 例えば以下のコードは C# 8.0 と 9.0 で意味が変わります。

class A
{
    record record;
}
  • C# 8.0: record と言う名前の型の、record という名前のフィールドになる
  • C# 9.0: record という名前のレコード型を定義

この破壊的変更に際して、そもそもとして、「record という名前の型自体に警告」というのも追加しています。

// C# 8.0 までは無警告。
// C# 9.0 で CS8860 警告を追加。
class record { }

lowerCase 型名要る?

record キーワードの件、 C# にしては珍しい規模の大胆な破壊的変更なんですが。

でも実際のところ、誰かこれで困った人はいらっしゃいます?

今のところ C# チームにもこれを問題視するクレームは全然入ってこないそうです。

そりゃまあ。 そもそも lowerCase な型名、C# では使わないですからね。

そして次の C# 11.0 候補として required というキーワードを足したいそうなんですが、 ここでも改めて「required という名前の型には警告」を出すかどうかが議題に上がりました。

ぶっちゃけ、record の時と比べると文脈を見た弁別は簡単だそうです。 とはいえ、record (この名前の方がよっぽど使われる可能性が高い)でも問題を起こさなかったんだから、いっそ required も警告にした方が幸せなのではないかと。

全部小文字 a~z 型名の禁止

ということで、将来キーワードとして被りかねない型名は全部警告にしてしまえという話があります。

C# において、キーワードはすべて「小文字のラテンアルファベットのみ」なので、小文字 a~z だけの型名を全部警告に。 Visual Studio 17.1 で導入される予定です。

本項の冒頭の class abc とか record xyz は Suggestion どころではなく、警告になります。 class var とか class dynamic とかもダメですよ!

ちなみに、どうしても小文字アルファベットな型名が必要な場合、@ を付けておけば警告は出ないそうです。

// class record だと CS8860 警告。
// class @record だと IDE1006 suggest だけ。
class @record { }

// class abc だと今後 CS8981 警告が出る予定。
// class @abc だと IDE1006 suggest だけ。
class @abc { }

小文字型名警告に備える

dotnet/runtime 内ではすでにこの警告追加に対する備え済み。

ほとんどはちゃんと UpperCamelCase になるようにリネームして対処しています。

一部、C/C++ との相互運用の場合は元の型名をそのまま引き継いだ方がいいという理由でそのまま。 これは @ を付けて対処したようです。

ニンジャキャット🐱‍👤

$
0
0

ニンジャキャット終了のお知らせ。

ニンジャキャット終了のお知らせ

そんなキャラはいなかった。いいね?

Windows の絵文字の絵柄一新

Windows 11 で Unicode 絵文字の絵柄が一新されると言われていたわけですが。 Windows 11 初期リリースでは変更がなく、先月ようやく新絵文字の一般提供開始されました。 そろそろ万人に(Dev 版とか Beta 版に登録してない人にも)届く頃ではないかと思います。

その結果が冒頭の絵文字。 ちなみに、同じものをこれまでの(Windows 10 とかの)絵文字で表示すると以下のようになります。

Windows 10 のとき

Windows オリジナル絵文字

この猫の絵文字、Windows のオリジナル絵文字です。 一応、ニュースになるくらいにはなってたんですが:

こいつ、元々は「内輪ネタ」だそうです。

このバージョンの Windows 10 (Build 14316)は ZWJ シーケンス(後述)に対応した初めてのバージョンっぽい(たぶん)ので、 それの社内テスト用に「内輪のキャラ」を使っていたんですかね。 なぜ公開した…

ちなみに、Windows 公式ブログでは「いろんな絵文字に対応したよ」というアピールはあるものの、 「オリジナル絵文字を足したよ」なんて言葉はどこにもないので、 特に「良かれと思って足した文字」ではないんじゃないかと思います。 ただの茶目っ気。

Unicode の仕様的な話

Unicode の絵文字がらみの仕様には何段階かあるんですが…

  • Graphme Cluster Boundaries
    • 「一連の文字を、ユーザーインターフェース上は1文字として扱え」という仕様
    • 例えば、「発音区別符号の手前で切ってはいけない」みたいなの
  • ZWJ シーケンス
    • Graphme Cluster の作り方の1つ
    • 接合子と呼ばれる文字の前後で切ってはいけない」という仕様
    • わりかし機械的に判定可能
    • 「複数の絵文字を ZWJ でつないで別の絵文字を作る」という仕様あり
  • RGI 絵文字
    • Recommended for General Interchange (一般にやり取り可能にすることを推奨)
    • 最低ラインどのベンダーでも実装してくれることを期待する絵文字の一覧
    • 1文字1文字リストアップしていて機械判定できない
  • RGI 絵文字 ZWJ シーケンス
    • ZWJ シーケンスとして定義されている RGI 絵文字
    • もし対応していない場合、ZWJ を無視して複数の絵文字で描画すればいいと言うことになってる

みたいな仕様があります。

(12月4日に書いたブログのネタもこの類です。)

ニンジャキャット終了

で、ニンジャキャットは、「RGI ではない ZWJ 絵文字シーケンス」ということになります。なので、

  • ZWJ シーケンスの仕様に沿って機械判定で「1文字扱い」はどのベンダーでもできる
  • RGI ではないので別に Windows 以外のベンダーが実装する義理は全くない
  • 対応していないベンダーでは単に「🐱 と 👤 の2文字」とかで描画すればいい

という文字。

で、冒頭の画像に戻るわけですが、

Windows 10 の頃:

Windows 10

Windows 11 (最近のアップデート):

Windows 11

はい、オリジナル絵文字だったものが、「対応していないので2文字で表示します」状態に変わりました。

元から本当に公開するつもりで作った絵文字なのかどうかすら定かではないですからねぇ。 絵柄一新時に追加するとも思えず…

消えたのは順当。 むしろ、IME の変換候補に痕跡が残ってることが問題…

まあ、「対応する必要性がない ZWJ シーケンス用のテストデータ」としては結構便利だったんですけどね。

Windows 曰く、環境依存

もう1個、IME の問題なんですけども… Windows の IME は「JIS X 0208 にない文字は全部環境依存扱いする」というのがありまして。

要するに Unicode が普及する前、Shift_JIS が主流、かつ、Windows (CP932) と Mac (MacJapanese) でそれぞれが Shift_JIS の独自拡張をしていた時代の名残り。

今となっては Windows でも Mac でも Linux でも iOS でも Android でも表示できる文字も「環境依存」扱いしてきます。 以下一例。

環境依存文字の例

ちなみに、ひとくくりに環境依存と言っても「何の環境に依存した文字か」が全然違います。

  • Unicode からの逆輸入で現在の Shift_JIS (Shift_JIS-2004) には入っている文字
    • スペード(♠): MacJapanese にあった文字
    • あお(靑): CP932 にあった文字
    • おんぷ(♬): Unicode 1.1 での追加
  • Unicode にもない文字
    • にんじゃきゃっと(🐱‍👤とか): Windows 以外で表示するつもりのない真に独自の文字

真に Windows 独自の文字と、今となっては Shift_JIS にすら入っている文字を同じ「環境依存」でくくるのはさすがにどうかと思うんですけどねぇ…

現代の環境依存文字

ちなみに、12月8日に書いたベンゼン環 ⌬ ⏣は、Unicode にはあるけども意図的に Shift_JIS には逆輸入されなかった文字です。

あと、Unicode に入っている以上、たいていの環境で表示はできる(実際、iOS でも Android でも大丈夫)ので、「環境依存」かと言われると微妙。

(まあ、こいつは IME では変換できませんが。文字コード直打ちからの変換とかネットで検索してコピペ以外の手段で入力する方法、一通りどの OS でも僕は知らないです。)

林檎

他に、MacJapanese にはあって Unicode には輸入されなかった文字として某林檎マークとかがあったりします。 「特定の一社のロゴマークとかは Unicode に採用しかねる」という理由で Unicode には入らず、 Mac、iOS では私用領域を使って林檎マークを表示しています。

私用領域なのでどこの誰がどういう文字のために使おうと自由です。 そういう意味で本当に環境依存。 どこかの誰かがこの文字コードに対して💩を表示しようとも文句は言えません。

今だったら🍎(U+1F34E、red apple)と🌈(U+1F308、rainbow)を使った ZWJ シーケンスとかで表現するんでしょうけどねぇ。 MacJapanese から Unicode への移行期には絵文字も ZWJ シーケンスの仕様もありませんでしたから。

任意色絵文字?

$
0
0

🐈‍⬛

Windows にもついに Unicode 13.0 が来ました(今更)。

Unicode 13.0

Unicode 13.0 のリリース、2020年3月なんですよね。 ずいぶんと前。

それに対して、Windows 10 の間は Unicode 13.0 には一向に対応せず… 今思えば新しい絵柄(Windows 11 の新絵文字)を作っていたから、古い絵柄(Windows 10 の絵文字)を更新するリソースを割かなかったんだろうなとは思うんですが。

Unicode 13.0 の新文字の分かりやすいのが本項冒頭の黒猫でして、 🐈‍⬛ の文字、Windows 10 だと 🐈⬛ (ネコ + 黒四角)になるはずです。

というか、Windows 11 でも対応したのはつい最近です。 こないだニンジャキャット終了のお知らせのときに書いた新絵文字のタイミングでやっと黒猫が表示できるようになりました。

iOS とかから遅れること1年半以上…

にじさんじの新人さん(今年7月デビュー)が 🐈‍⬛ を推し絵文字に決定した時には「やべ、Windows で表示できねぇ…」ってなって焦りました。

色選択

さてこの黒猫、典型的な ZWJ シーケンスです。 最近書いたUTF-8の敗北話とか、 ニンジャキャット終了のお知らせとかでも触れてるんですが、

🐈 (U+1F408)、ZWJ (U+200D)、⬛ (U+2B1B)

という3文字から構成される、🐈 の色違い絵文字です。

ちなみにこの ⬛ なんですが、「Black Large Square」という名前の文字です。 そして、実は Large Square シリーズ、現在(Unicode 12.0 以降)、他に7色あります。

  • 🟥 (U+1F7E5) Large Red Square
  • 🟦 (U+1F7E6) Large Blue Square
  • 🟧 (U+1F7E7) Large Orange Square
  • 🟨 (U+1F7E8) Large Yellow Square
  • 🟩 (U+1F7E9) Large Green Square
  • 🟪 (U+1F7EA) Large Purple Square
  • 🟫 (U+1F7EB) Large Brown Square

これもしや、もはやゲーミング動物絵文字が作れるのでは…

※画像はイメージです

ゲーミング猫 ※画像はイメージです

どう見ても「文字コード」の仕様の範疇を超えてますけども。

実際、こういうのは「文字」のレイヤーの1段上の「マークアップ」とかを使ってやってくれ(要するに、Unicode の債務ではなく、HTML の style 属性とか CSS とかを使って色を付けて欲しい)ということになってたはずなんですけども。

絵文字で Unicode に色の概念を持ち込んじゃったから… むしろ今、カラー絵文字には foreground-color が効かないですからねぇ。

まあ、こんなカラフル動物絵文字が RGI (どのベンダーも実装すべきという推奨絵文字)に採用されるとは思えませんが。 むしろ、なんで黒猫を足しちゃったんですかね…

某4色窓

そしてこのカラフル四角形があるなら…

🟥🟩
🟦🟨

あのロゴ行けるんじゃない? ZWJ シーケンスで四角を4つ繋げば。

みたいな話も C# ライブ配信中ではよくコメントが付いたりします。

まあ、先日、権利的にまずそうなロゴは Unicode に採用しかねるという話を書いたところなので、こんな絵文字が採用されることはあり得ないんですけども。

特にこの会社、ロゴ利用のガイドラインがものすごくしっかり規定されてるんで、簡単に色々抵触しそうですし。

悪い意味でお気に入りの文字 〠〄

$
0
0

こないだ書いたベンゼン環の話ニンジャキャット終了のお知らせはある意味前振りだったりしまして。

個人的にやべぇなと思う Unicode の文字で1・2を争うのは 〠 と 〄 だったりします。

  • 〠: U+3020、postal mark face、顔郵便マーク
  • 〄: U+3004、Japanese industrial standard symbol、JIS マーク

〠〄

アップルロゴの話で書きましたが、 商標になるようなものを Unicode に入れるとか普通は考えられないわけですが。 〠 と 〄 は一体…

出どころ

2文字とも出自は MacJapanese でして。 要は Shift_JIS の Mac 独自拡張。

どうも元々は日本の中小印刷所が使っていた外字だったらしい? それがデファクトスタンダード化して Mac OS に取り込まれ。 それとの「互換性用文字」として Unicode にも含まれてしまったという経緯。

商標的にも怪しいですし、今の Unicode は「1国のローカルでだけ通用する記号の類」を文字として追加することには否定的なんで、今の基準でいうと絶対に入らなさそうな文字です。 ただ、MacJapanse としてもう流通してしまっている以上、互換性用としては必要。

とはいえ、商標の権利上の懸念からアップルのロゴは Unicode には収録されなかったわけで、 じゃあどうして 〠 と 〄 は平気だったのか… 私企業じゃく公的な物だとか、商標登録されてるかとかの差はあるでしょうけども。

郵便番号を 〒 で表すこと自体、日本独自です。 この記号は逓信(ていしん)の「テ」か「T」から来てるとする説が有力らしく、今となってはその元ネタの「逓」の文字すら使われておらず。

まして顔の方。 郵政省のマスコットですよね?

一応、正確に言うと、〠 に手と胴体の付いたキャラクターが「ナンバーくん」という郵政省(郵政民営化前)のマスコットだったみたいです。 (「胴体が付いてないからナンバーくんではない」と言えなくもない。)

そしてこの「ナンバーくん」は「役割を終えた」、「民営化に伴い新会社では『撤去が望ましい』」とか言われているそうで。 日本郵便のマスコット キャラクターとしては今現在「ポストンくん」という別キャラがいるそうです。

ちなみに、Wikipedia のポストンくんのところを読んでると、「フォントのメイリオでは、ベータ版公開時には郵便マークのコードポイントのU+3020にポストンの記号が使用されていた」とか書かれていたりします。 要は、互換性のためだけに存在する文字(しかも権利的に怪しい文字)に対して字形変更をすべきかどうか問題が発生。

要は JIS マーク。 「JIS 適合していることを認証された」というのを1文字で印刷所に送れるのは昔は重宝したんでしょうね…

ところでこの記号、よく見てください。 いわゆる「旧 JIS マーク」になっています。 2004年に工業規格の法改正に伴って JIS マークも刷新されています。 「ポストンくん」問題同様、こっちでも「Unicode 中の 〄 もグリフ字形すべき?」みたいな話があったりします。

新字形の文字コード

ここでまあ、ベンゼン環は ⌬ と ⏣ の2文字あるという話につながります。

JIS マークも「やるんだったら新旧両方の字体があるべきだろう」という所までは話した出ており。 現実的には、元々の 〄 (U+3004) に異体字セレクターでも付けるのがいいんじゃないかということになっています。

ちなみに、需要がなさ過ぎて具体的な議論は進んでいないので、 そんな文字が Unicode に追加されることはまあまずないと思います。

ただ、GlyphWiki (共同編集で保守されている文字字形 Wiki)には 〄 のバリエーションとして新 JIS マークのページがあったり、 一部の DTP 向けフォントには外字として新 JIS マークがあるらしかったり、 今でも日本には「文字として JIS マークを印刷したい」需要はあるんですね…

まとめ

ということで改めて 〠 と 〄、

  • 互換性のためだけにある
  • 権利関係が謎
  • しかも今現在、新意匠が作られたのでもう使われなくなった字形
  • Unicode 的にも字形変更すべきか議論がある

というなかなかに罪深い文字だったりします。

なので絵文字に関する地雷話をしてるときとかに、「まあ絵文字じゃなくてもこんなやべぇやつがいるんで今更」みたいな話をよくしたり。

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

$
0
0

11月くらいからなんとか消化し始めたC# ライブ配信で口頭では言ったけどブログ化はしてなかったやつ」、 「C# 10.0 の補足」とか、文字コード・絵文字がらみの雑談話を抜けて、 やっと「C# 11.0 候補」の話になります。

こんな時間かかるかー…

半自動プロパティ

C# 11 目標で、自動プロパティにちょっと手が入りそうです。

バッキング フィールドfield キーワードで読み書きできるようにするというもの。 俗称「半自動プロパティ」。

おさらい: 初期 C# のプロパティ

C# 1.0 の頃からの一番煩雑な書き方だとプロパティは以下のように書いていました。追加でフィールドが1個必要。

class A
{
    private int _x;
    public int X { get { return _x; } set { _x = value; } }
}

それに対して C# 3.0 で書けるようになった簡易記法が自動プロパティ(automatically implemented property、通称 auto-property)。 get; set; だけ書くと、上記の _x フィールド相当のものを自動的に作ってくれます。

class A
{
    public int X { get; set; }
}

自動プロパティが使えなかったものの例

(この後もプロパティは細かく色々な改善があるんですが、それは置いておいて)

C# 3.0~10.0 までの “完全に自動な” プロパティだと一部の頻出する用途に使えなくて、結局は自前でフィールドを用意しないといけないことがありました。 特に有名な例を2つ挙げると、

1. PropertyChanged

using System.ComponentModel;
using System.Runtime.CompilerServices;

class A : INotifyPropertyChanged
{
    private int _x;
    public int X { get => _x; set => SetProperty(ref _x, value); }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void SetProperty<T>(ref T storage, T newValue, [CallerMemberName] string? propertyName = null)
    {
        storage = newValue;
        PropertyChanged?.Invoke(this, new(propertyName));
    }
}
  1. 遅延初期化
class A
{
    private string? _x;
    public string X => _x ?? GetX();

    private string GetX()
    {
        // 初回限りの重たい処理
    }
}

field キーワードの追加

で、要望自体は結構昔からあったんですがようやく C# 11.0 で採用されそうなのが「field キーワード」。

例えば前節の例は以下のように書けます。

1. PropertyChanged

class A : INotifyPropertyChanged
{
    public int X { get => field; set => SetProperty(ref field, value); }

    // 以下元と同じ
}
  1. 遅延初期化
class A
{
    public string X => field ?? GetX();

    // 以下元と同じ
}

細々補足

以下のような補足あり。

  • C# らしく(破壊的変更を避けて)、field は文脈キーワード
    • field と言う名前のフィールドがない場合だけキーワード扱い
  • キーワード扱いを受けた場合、nameof(value) はコンパイルできないという仕様。
  • get 側しかない場合は get-only プロパティと同様
    • コンストラクターでだけ set 可能
    • 生成されるフィールドは readonly 扱い

この新機能、俗称としては「半自動プロパティ」(semi-auto-property)なんですが、実装上・仕様書上は「自動プロパティの項目を修正」みたいです。

元:

  • セミコロンのみの get; set; しかないプロパティを自動プロパティと呼ぶ

変更後:

  • 以下の2つを自動プロパティと呼ぶ
    • セミコロンのみの get; set; アクセサーしかないプロパティ
    • アクセサー内で field キーワードを使っているプロパティ

おまけ field はキーワードで value は変数?キーワード?

ちょっと余談。

field は明確に「文脈キーワード」です。補足説明の通り、nameof(field) 不可。

ところで、じゃあ、C# 1.0 の頃からある value はと言うと… とりあえず、Visual Studio 上では「靑」(キーワード扱いの色)です。 (↓ うちのサイトの色付けは Visual Studio 初期設定準拠。)

class A
{
    private int _x;
    public int X { set => _x = value; }
    // ↑ Visual Studio 上、value の文字は青(キーワードの色)になってる。
}

ところで、この value、仕様書上は「set アクセサーには暗黙の引数 value がある」みたいな書かれ方になっています。 そして、結果的に nameof(value) は許されるという。

class A
{
    private string _x;
    public int X { set => _x = nameof(value); }
    // 意味あるコードではないものの、とりあえずコンパイル可能。
}

nameof(int) とかも許されておらず、nameof の中に「青」が来る(たぶん)唯一の例となります。

時代の名残りと言うかなんというか… 今なら value も文脈キーワードにしたかもしれないですね。

ちなみに、同じく仕様からして「暗黙の引数」とされているトップ レベル ステートメントコマンドライン引数の args はちゃんと「群青」(変数・引数の色)です。

Console.WriteLine(args[0]);

まあ、field キーワードは最初から「キーワード扱い」の予定です。

class A
{
    public int X { set => field = value; }
}

【C# 11 候補】 UTF-8 リテラル

$
0
0

.NET の UTF-8 対応がらみの続報。

byte でやりくり

元々、string (UTF-16 でデータを持ってる)に加えて Utf8String みたいな名前で UTF-8 な型を追加しようか何て話もあったんですが。 stringUtf8String の2重管理がしんどいだろう、これだけ string 前提で .NET エコシステムが確立された状況で追加は無理だろうという雰囲気になっています。

string の中身を UTF-8 に変更した方が建設的かもしれないという話も出るくらいですが、さすがにそれをやりだすと大工事過ぎて短期では無理でしょう。 著者個人的にも「10年先ならわからないけども」くらいのお気持ちになりつつあります。

そうこうしているうちに、「生 byte 列で UTF-8 を扱う」と言うのが .NET エコシステム内でデファクトスタンダード化してしまいました(今ここ)。 例えば System.Text.Unicode 名前空間中のメソッドは以下のような感じになっています。

using System.Buffers;

namespace System.Text.Unicode;

public static class Utf8
{
    public static OperationStatus FromUtf16(
        ReadOnlySpan<char> source, Span<byte> destination, out int charsRead, out int bytesWritten,
        bool replaceInvalidSequences = true, bool isFinalBlock = true);

    public static OperationStatus ToUtf16(
        ReadOnlySpan<byte> source, Span<char> destination, out int bytesRead, out int charsWritten,
        bool replaceInvalidSequences = true, bool isFinalBlock = true);
}

Span<byte>ReadOnlySpan<byte> で UTF-8 文字列を扱っています。

文字なのかその他のバイナリ形式なのかがわからなくなるんであんまり親切設計ではないんですが… 型変換やオーバーロードをあんまり増やすのもしんどく、 「生 byte 列で UTF-8 を扱う」は結構定着しちゃうんじゃないかという感じ。

リテラル問題

とはいえ。 UTF-8 扱いで Span<byte> とかを使うにあたって困るのが文字列リテラル。 今だと以下のように byte 定数的に new byte[] するしか方法がありません。

ReadOnlySpan<byte> _true = new byte[] { (byte)'t', (byte)'r', (byte)'u', (byte)'e' };
ReadOnlySpan<byte> _false = new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
ReadOnlySpan<byte> _null = new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' };

一応、これ、最適化はされて new byte[] のヒープ アロケーションは発生せず、 直接 DLL 中のデータ領域からデータが読まれます。

この3つくらいならいいんですけども、極まってくるとありとあらゆる文字列リテラルを UTF-8 byte 列化したくなり…

とかを見てもらえるとなかなかにつらみを感じてもらえるのではないかと思います。

"100" みたいなものすら new byte[] { (byte)'1', (byte)'0', (byte)'0' }

UTF-8 文字列リテラル

と言うことで着地点として、リテラルだけ UTF-8 なものを用意しようかという雰囲気になっています。

  • Span<byte>ReadSpan<byte> に対して文字列リテラルを渡すと自動的に上記のような UTF-8 byte 列を生成する
  • オーバーロード解決や var 型推論用に u8 接尾辞を用意

例えば以下のように書けるようになります。

暗黙的変換:

byte[] array = "hello";             // new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20 }
Span<byte> span = "dog";            // new byte[] { 0x64, 0x6f, 0x67 }
ReadOnlySpan<byte> span = "cat";    // new byte[] { 0x63, 0x61, 0x74 }

u8 接尾辞:

string s1 = "hello"u8;      // エラー。型が合ってない。
var s2 = "hello"u8;         // Ok。型は ReadOnlySpan<byte>。
Span<byte> s3 = "hello"u8;  // Ok。
byte[] s4 = "hello"u8;      // Ok。

UTF-8 として不正になる文字列リテラルはコンパイル エラーにするそうです。 .NET の文字列は UTF-16 というか実際には「古き良き Unicode」(2バイト固定長で行けると思ってた頃の Unicode)なので、「サロゲート ペアの片割れ」みたいな今となってはダメなやつを受け付けてしまうので。

byte[] array = "\uD801"; // ハイ サロゲートのみ。コンパイル エラーにする。

ちなみに、const string から UTF-8 リテラルも作れるし、 「不正な UTF-16 を + でつないで、その結果が有効な UTF-8 になるなら OK」だそうです。

const string first = "\uD83D";  // ハイ サロゲート。
const string second = "\uDE00"; // ロー サロゲート。
ReadOnlySpan<byte> span = first + second; // これは OK

Utf8String 型の可能性

前述の通り、今の string を置き換えるような Utf8String 型が追加される可能性はかなり低くなってきたんですが。

一応まだ可能性 0 とは断じない方がいいので、一応この仮定的な Utf8String の存在は考慮しているそうです。

もしも Utf8String が積極的に使いたい「良い型」になったとしても、 たぶん、"" から byte[]Span<byte>ReadOnlySpan<byte> への暗黙的変換は対して問題にならなさそう。

後悔するとしたら u8 接尾辞の「自然な型」を ReadOnlySpan<bte> にしてしまう点で、これに関しては「やっぱり Utf8String に変えたい」となっても変えれるものではなくなります。 とはいえ、「なので今は自然な型を決めるのはやめておこう」と思うほどのものではない(ので、ReadOnlySpan<bte> な方針でいく)でしょう。


【C# 11 候補】リスト パターン【VS 17.1 p2 で追加予定】

$
0
0

C# にパターンがまた1個増えます。 今回はリスト。is [..] とかで配列や List<T> にマッチ。 これをリスト パターンと言います。

Roslyn 化(C# コンパイラーを C# で書き直し)した初期の頃から、C# の進化の長期テーマになってる "Programming With Data" の続きです。 以下の表の赤丸を付けたところ。

リスト パターンの立ち位置

ちなみにこのリスト パターンは Visual Studio 17.1 Preview 2 向けですでに merge 済み。近々動くコンパイラーを実際に触れるはずです。

角括弧

リスト パターンには [] を使うことになりました。

当初予定は {} (プロパティ パターンと被る)とか []{} (これはこれでキモイ)とかも検討されていたんですが。 配列初期化子とかコレクション初期化子との対称性のためでしたが、 構文解析的にきつくて断念。

var array = new[] { 1, 2 };

// 当初案1:
// int[] array = { 1, 2 }; との対比。
// { Length: > 0 } とかとの区別が付かなくて断念。
if (array is { })
{
}

// 当初案2:
// var array = new[] { 1, 2 }; との対比。
// まだ {} の部分がきついのと、length を必要としないときに [] を付けるのがだいぶつらい。
const int length = 2;
if (array is [length] { 1, _ })
{
}

結果的に、[] だけにすることに。

var array = new[] { 1, 2 };

Console.WriteLine(array is []); // 長さ0マッチ。false。
Console.WriteLine(array is [_, _]); // 長さ2マッチ。true。
Console.WriteLine(array is [ .. ]); // 任意長さマッチ。true。

Console.WriteLine(array is [ 1 ]); // 長さ1マッチ。false。
Console.WriteLine(array is [ 1, .. ]); // 1で開始、任意長さ。true。
Console.WriteLine(array is [ .., 2 ]); // 2で終了、任意長さ。true。
Console.WriteLine(array is [ 1, .., 2 ]); // 1で開始、2で終了、任意長さ。true。

基本的には「長さピッタリ」にだけマッチします。 任意長さとマッチさせたい場合は .. を挟むという仕様です。

..パターン

ちなみに、 .. の後ろには入れ子でパターンを書けます。 主に var パターンで「マッチ結果の一部分」を抜き出すのに使います。

ReadOnlySpan<int> a = new[] { 1, 2, 3, 4, 5 };

if (a is [1, ..var middle, 5])
{
    Console.WriteLine(middle.Length); // 2, 3, 4 で長さ3
}

あんまり意味はないですが、[..[]] とかも書けます。

ReadOnlySpan<int> a = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(a is [1, ..[2, 3, 4], 5]); // true

[1, ..[2, 3, 4], 5][1, 2, 3, 4, 5] が同じ意味になるので、 ある意味スプレッド演算(JavaScript とかにある配列を展開するやつ)です。

展開結果

リスト パターンは、Length チェックとインデックス・範囲処理を使ったようなコードに展開されます。

例えば先ほどの a is [1, ..var middle, 5] であれば、以下のようなコードと同じ結果になります。

ReadOnlySpan<int> a = new[] { 1, 2, 3, 4, 5 };

if (a.Length >= 2 && a[0] == 1)
{
    var middle = a[1..^1];
    if (a[^1] == 5)
    {
        Console.WriteLine(middle.Length);
    }
}

^.. もさらに展開すると以下のコードと同じ意味。

ReadOnlySpan<int> a = new[] { 1, 2, 3, 4, 5 };

var length = a.Length;
if (length >= 2 && a[0] == 1)
{
    var middle = a.Slice(1, length - 1 - 1);
    if (a[length - 1] == 5)
    {
        Console.WriteLine(middle.Length);
    }
}

ちなみに、LengthCount プロパティとインデクサーを持っている型に対してリスト パターンを使えます。

[] リテラル (C# 11 より後かも)

new[] {} との対称性をあきらめてパターン側を [] にしたわけですが、 ここで逆の発想が出て来たみたいです。 配列・コレクションの初期化の方も [] リテラルでやる案。

using System.Collections.Immutable;

int[] array = [ 1, 2 ];
Span<int> span = [ 1, 2 ];
ReadOnlySpan<int> ros = [ 1, 2 ];
List<int> list = [ 1, 2 ];
ImmutableArray<int> immutable = [1, 2];

これの話はまた回を改めて書くと思いますが、ImmutableArray の初期化も視野に入れています。 (ImmutableArray は今の new() { 1, 2 } だと望まれる動作にならない。)

ImmutableArray に対してコレクション初期化子

$
0
0

Immutable コレクションは現状いろんな使いにくさがあって悪名高いわけですが。 今日は「リスト パターンの回に出した [] リテラルの話」と絡むので、特に ImmutableArray とかに対してコレクション初期化子が使えないという話をします。

クイズ

以下のようなコード。

using System.Collections.Immutable;

ImmutableArray<int> a = new() { 1, 2 };

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

どういう結果になるでしょう?

  • 1, 2, 3 が表示される
  • 何も表示されない
  • foreach のところで例外が出る
  • new のところで例外が出る
  • コンパイルできない

ちなみに、C# ライブ配信中に参加者みんな間違えてました

答え

new のところで例外です。 しかもぬるぽ。

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Collections.Immutable.ImmutableArray`1.get_Length()
   at System.Collections.Immutable.ImmutableArray`1.Add(T item)
   at Program.
$(String[] args) in C:\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:line 4

そして、どうしてこうなるかの説明にも何段階かの変形が必要という…

とりあえず、foreach のところは無罪というか、その行までたどり着かないのでいったん削除。 (ちなみに、もしたどり着けた場合、foreach でも例外が出ます。)

using System.Collections.Immutable;

ImmutableArray<int> a = new() { 1, 2 };

コレクション初期化子は、以下のように、new() の後に Add メソッドを呼ぶという展開のされ方になります。

using System.Collections.Immutable;

ImmutableArray<int> a = new();
a.Add(1); // ぬるぽるのはこの行になる。
a.Add(2);

まあ、この時点ですでに問題の原因が分かってくる頃かと思いますが、一応もう1段。 ImmutableArray は構造体で、C# 10.0 より前には構造体に引数なしコンストラクターがなかったので、これは以下のコードと同じ意味になります。

using System.Collections.Immutable;

ImmutableArray<int> a = default;
a.Add(1); // ぬるぽるのはこの行。
a.Add(2);

構造体の default は「全ビットを0にする」みたいな扱いで、 参照型の場合には null が入ります。

要するに、実質以下のコードと同じような挙動になります。

// 実質これと同じ結果
using System.Collections.Immutable;

ImmutableArray<int> a = ImmutableArray.Create<int>(items: null);
a.Add(1); // ぬるぽる原因は items: null なせい。
a.Add(2);

ちなみに、「既存の構造体に引数なしコンストラクターを足すのは破壊的変更」なので、破壊的変更を極力割けて通っている .NET 的に、追加されることはないと思います。なのでたぶん、ImmutableArray<T> がこんな変な挙動から解放される未来もないと思われます。

また、以下のようにぬるぽ回避コードを入れても、おそらくほとんどの人にとって所望の結果にはならないと思います。

using System.Collections.Immutable;

// ぬるぽ回避
ImmutableArray<int> a = ImmutableArray.Create(Array.Empty<int>());
a.Add(1); // 無事通過。
a.Add(2);

// ただし、a の中身は Empty のまま。
// まあ、immutable ですからね。初期状態から変わるはずないですよね。
foreach (var x in a)
{
    Console.WriteLine(x);
}

ええ、immutable ですから。 Add は「元の配列に1要素 append した新しい配列を作って返す」という仕様。 自身の書き換えはしません。

課題

2つの問題が重なってこんなことになっています。

  • コレクション初期化子に適さないのに、C# の構文上はコレクション初期化子が使える条件を満たしてしまっている
  • default が不正な状態になる構造体を作ってしまっている

と言うことで、また日を改めて書くと思うんですが、それぞれ以下のような解決策が考えられています。

  • 今後追加予定の新構文の [] リテラルでは、ImmutableArray でも使えるような展開の仕方をする
  • 基本的に default 禁止なアノテーション(null 許容参照型の構造体版)を作る

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

$
0
0

今日はリスト パターンの回でちょこっと出て来た [] リテラルの話

逆に、リスト パターン側でも {} ではなく [] を使う決断に至った理由でもあります。

もう実装があるリスト パターンと違って、こちらはまだ案が出たてで、 もしかしたら C# 11 よりもさらに後になるかもしれないです。

[] リテラルの導入

元々、C# よりも後に世に出たり、大幅改修したことがあるプログラミング言語には結構「コレクション リテラル」系の文法があります。 で、多くの場合、[ 1, 2, 3 ] みたいに角括弧を利用。

そして現在の C# には new[] { 1, 2, 3 } みたいな書き方はあるにはあるものの、いろんなコレクション型があって、それぞれ書き方に統一感がない状態。

// 型を明示、かつ、配列の時に限り {} だけで OK。
int[] array1 = { 1, 2, 3 };

// 型推論を使いたければ new[] {}。
var array2 = new[] { 1, 2, 3 };

// Target-typed new + コレクション初期化子。 () は省略不可。
List<int> list1 = new() { 1, 2, 3 };

// 通常の new + コレクション初期化子。こっちの場合は () 省略 OK。
var list2 = new List<int> { 1, 2, 3 };

// Span にはまあ、new で配列を割り当ててもいいものの、
// パフォーマンス的には stackalloc を使った方が大体の場合有利。
Span<int> span = stackalloc int[] { 1, 2 };

// ReadOnlySpan も同様。
// あと、stackalloc の後ろは型推論で省略可能。
ReadOnlySpan<int> ros = stackalloc[] { 1, 2, 3 };

// new() もコレクション初期化子も使えないかわいそうな型あり。
var immutable = System.Collections.Immutable.ImmutableArray.Create(1, 2, 3);

C# でももう少し統一感あるコレクション リテラルがあった方がいいし、 だったら他の言語に倣って [] を使った新文法を導入でいいのではないかという話になります。

// ぜんぶ [] にしたい。
int[] array1 = [ 1, 2, 3 ];
List<int> list1 = [ 1, 2, 3 ];
Span<int> span = [ 1, 2, 3 ];
ReadOnlySpan<int> ros = [ 1, 2 ];
System.Collections.Immutable.ImmutableArray<int> immutable = [ 1, 2, 3 ];

そしてこっち(リテラル側)でも [] を使うのであれば、 パターンの方{} (プロパティ パターンと区別が付かない)とか []{} (new[]{} との対称性はいいかもしれないもののキモい)とか考えず、そっちも素直に [] を使えばいいということに。

[] リテラル中の .. (spread 演算)

パターンの方で「[1, ..[2, 3, 4], 5][1, 2, 3, 4, 5] が同じ意味になる」と書きましたが、コレクション リテラル中でも同じく「入れ子のコレクションを展開」みたいな仕様があります。

int[] array = [ 1, 2, 3 ];
List<int> list = [ 0, ..array, 4 ]; // 0, 1, 2, 3, 4

他の言語で unpacking とか splat (* 記号が一部の人にそう呼ばれていて、この機能に * を使ってる言語ではこう呼ぶ)とか spread (拡散)演算子とか呼ばれているやつです。

C# ではまあ、LINQ の Concat, Append, Prepend とかを使って同様のものは書けていましたが、煩雑、かつ、パフォーマンスはいまいちでした。

int[] array1 = { 1, 2, 3 };
int[] array2 = { 4, 5, 6 };

// enumerator のインスタンスが余計に new されたりで遅い。
var linq = array1.Concat(array2).Prepend(0).Append(7);

// 列挙も結構遅い。
foreach (var x in linq)
{
    Console.WriteLine(x);
}

// LINQ のよりも速い実装になる予定(後述)。
// かつ、Preapend よりはだいぶわかりやすい。
var spread = [ 0, .. array1, .. array2, 7 ];

おまけ: {} 案

一時期はパターンの方も is {} にしたいみたいな話もあったんですが。 元々配列初期化子が {} ですし、コレクション初期化子も {} になる案もなくはなかったです。

ただ、{} の用途としては他に Expression blocks という提案も出ていて、それとの弁別が無理そうということで没。

展開結果

展開結果、基本的には「前から順に詰める」です。 配列の場合だと割かしシンプルで、例えば以下のような感じ。

int[] array1 = { 1, 2, 3 };
int[] array2 = { 4, 5, 6 };

// var spread = [ 0, .. array1, .. array2, 7 ];

var len = 1 + array1.Length + array2.Length + 1;
var spread = new int[len];

var i = 0;
spread[i++] = 0;
for (int j = 0; j < array1.Length; j++, i++) spread[i] = array1[j];
for (int j = 0; j < array2.Length; j++, i++) spread[i] = array2[j];
spread[i] = 7;

Span<T> の場合には new T[] のところを stackalloc T[] に変更。 ReadOnlySpan<T> の場合はいったん Span<T> と同じ処理でデータを書き込んでから、最後に ReadOnlySpan<T> に変換。

それ以外の型については「所定のパターンを満たすコンストラクターと Init メソッドを呼ぶ」と言うことになっています。

  • capacity という名前の引数があるコンストラクターがある場合はそれを、なければ引数なしコンストラクターを呼ぶ
  • void Init(T1) があって、T1T[] なら new[] で、T1Span<T>, ReadOnlySpan<T> なら stackalloc[] で一時バッファーを作ってから Init メソッドに渡す

例えば Init(int[]) だけ持っている型だと以下のような感じ。

// A a = [ 1, 2, 3 ];
int[] tempA = { 1, 2, 3 };
A a = new();
a.Init(tempA);

class A
{
    public void Init(int[] items) { }
}

capacity コンストラクターと Init(ReadOnlySpan<int>) を持つ型だと以下のような感じ。

// A a = [ 1, 2, 3 ];
ReadOnlySpan<int> tempA = stackalloc[] { 1, 2, 3 };
A a = new(3);
a.Init(tempA);

class A
{
    public A(int capacity) { }
    public void Init(ReadOnlySpan<int> items) { }
}

immutable コレクション初期化

ちょっと別の機能追加も必要なのでさらに不透明なんですが、 この [] リテラルは前に話した ImmutableArray の初期化問題の解決策としても期待されています。

とりあえず、ImmutableArray についても前節と同じルールで初期化を掛けることを考えます。

using System.Collections.Immutable;

// ImmutableArray<int> a = [ 1, 2, 3 ];
ReadOnlySpan<int> tempA = stackalloc[] { 1, 2, 3 };
ImmutableArray<int> a = new();
a.Init(tempA); // こういうメソッドを足したいという話。今はない。

こういう Init メソッドを足せればいいわけですが、 immutable を名乗る以上、new() とは別に呼ばれるとまずいという話になります。

で、そこはinit-only プロパティと同じ方式で乗り切りたいとのこと。

任意のメソッドに対して、new() 中、もしくは、直後にしか呼ばない・呼ばれない保証をコンパイラーがするような仕様(メソッドに対する init 修飾)があればいいわけで、そういう仕様も模索中とのこと。

struct ImmutableArray<T>
{
    readonly T[] _items;

    // init 修飾を付けたメソッドは new() 内、もしくは、直後でしか呼べないように、
    // コンパイラーが呼び出し箇所をチェックする。
    public init void Init(ReadOnlySpan<T> items)
    {
        // 本来、コンストラクター内でしか書き換えてはいけないはずのフィールドを、
        // init 修飾子が付いたメソッド内に限り書き換え可能にする。
        _items = items.ToArray();
    }
}

【C# 11 で再検討】Countable

$
0
0

リスト パターンの実装で、「Count もしくは Length を持った型なら何にでも使える」と説明しました。 C# ではこれを「Countable」と呼んでいます。

この Countable というコンセプト、最初に出て来たのは C# 8.0 の Index のときです。 で、リスト パターンやコレクション リテラルでも Countable が出て来たところで、 Index に対する Countable の扱いも拡張しよう見たいな話がちょこっと出ています。

ちょっと課題もあるので C# 11 時点はおろか、その先でもどうなるかわからないですがとりあえず提案が出ました。

Countable

Index 構文では、^i みたいな書き方で「末尾要素から i 個前」みたいなインデックスを表します (一方、「先頭から」の方は普通に int を使います)。 「末尾から」を取るためにはコレクションの要素数を必要としますが、 これはパターン ベースな構文になっていて、以下の条件を満たす型なら何でも使えます。

  • Index 型を受け取るインデクサーがある場合は x[^i]^iIndex.FromEnd(i) として解釈される
  • int 型を受け取るインデクサーがあって、
    • int 型の Length プロパティを持っている場合、x[^i]x[x.Length - i] に展開する
    • それ以外で、int 型の Count プロパティを持っている場合、x[^i]x[x.Count - i] に展開する

この条件を満たす型を Countable と言います。 リスト パターンも Countable な型に対して使えます。

Index サポートの拡張

C# 8.0 時点ではインデクサーだけに対応しました。 前述の通り「x[^i]x[x.Length - i] に展開する」みたいな処理が掛かります。

一方、RemoveAt メソッドとかでも同様に RemoveAt(^i) を使いたいという話があります。 C# の文法的に対応しなくても、ライブラリ側で RemoveAt(Index) なオーバーロードを足せば今でも RemoveAt(^i) と書けます。 ただ、全てのコレクション型に対してオーバーロードを足して回るというのも大変なので、改めて「Countable のサポート範囲の拡張」の話が出てきました。

問題1: Add(int) が誤判定しそう

パターン ベースな構文は無節操にやると結構「意図せずパターンを満たしてしまった」と言うことが起こります。 今回の場合も、以下のような書き方ができてしまうという懸念あり。

List<int> list = new();
list.Add(^1);

これが list.Add(list.Count - 1) と解釈されかねないわけで、それが望まれる挙動にはならないでしょう。

一応、この問題は「Add などの引数で、引数名が index の時にだけこの構文を使える」みたいな制限を掛けることである程度の対処はできそうです。

問題2: Range にも似た処理が欲しい?

Index^i のサポート範囲を拡張するんなら、 Rangei..j も拡張したいという話も自然と出てきます。 list.RemoveRange(1..^1) みたいなので一定範囲の要素をまとめて削除みたいな感じで。

とはいえ、こちらは今現在デファクトになっているパターンがないですし、 どうせオーバーロードを今から足すのであれば RemoveRange(Range) なオーバーロードを足せば、C# の文法の拡張なしでライブラリだけで対応できます。

また、RemoveRange(1..^1) の部分を RemoveRange(1, list.Count - 2) みたいな感じで、引数の数が増えるような展開が必要になるのでちょっと分かりにくい仕様になります。

問題3: オーバーロード解決

新機能を足すたびに問題になるオーバーロード解決なんですが…

RemoveAt(^i) みたいなやつでも、下手な実装をすると破壊的変更になりかねません。 例えば今現在、インスタンス メソッドとして RemoveAt(int) があって、拡張メソッドとして RemoveAt(Index) があったとして、 新構文で RemoveAt(^i)RemoveAt(Count - i) に展開するようになると、RemoveAt(int)RemoveAt(Index) のどちらを呼ぶかが変わってしまうことがあります。

今でも起こりうるパターン ベース問題

Vector<T> 型(System.Numerics 名前空間)はインデクサーを持っているわけですが、以下のようなリスト パターンを書きたいかどうかという話もあります。

using System.Numerics;

Vector<byte> v = new();

Console.WriteLine(v is [ 0, .., 0 ]);

これを書けるようにするには Vector<T> を Countable にすればいいんですが… Vector<T> の文脈的に「Length と言われても要素数には見えない」という別問題が発生します。 「ベクトルの Length」というと多くの場合はユークリッド距離を指していて、要素数ではありません。

ということで、なんらかの opt-out 手段、すなわち、「Countable になりそうだけど Countable 扱いしてほしくない」というのを表明する手段も必要になるんじゃない?という提案も出ていたりします。

こないだ ImmutableArray の話で書いたみたいに、 望まれない形でたまたまパターンを満たしてしまった場合、 結構悲惨なことになりますんで…

【C# 11 候補】defaultable value type

$
0
0

ImmutableArray に対してコレクション初期化子は使えないという話でちょっと出しましたが、この問題の原因の1つは「既定値(default、0初期化)のまま放置してはいけない型がある」というものです。

default 放置問題は「null を null のまま放置してはいけない」という問題に直結するので、 null 許容参照型とも関連します。

ということで「クラスの null 解析と同様に、構造体の default に関するフロー解析を行う」という提案が前々からあるんですが。 状況としては「提案のドラフトは書いてみたけど、まだ思い悩んでる点があって、Design Meeting に議題を上げる段階にない」みたいな感じです。

default 放置問題

C# 8.0 でnull 許容参照型(nullable reference type、通称 NRT)が入って、以下のように、null 参照例外が出そうな箇所にはコンパイル時に警告を出してくれるようになりました。

#nullable enable

// 警告: ? が付いてない変数に null を渡してる。
string s = null;

// この行でも警告: s に null が入ってることを認識してる。
Console.WriteLine(s.Length);

// OK
string? n = null;

// 警告: null かもしれないもののメンバー参照してる。
Console.WriteLine(n.Length);

// これなら OK: not null 判定してるのでメンバー参照してももう大丈夫。
if (n is not null) Console.WriteLine(n.Length);

この解析は「できる範囲で、できることからやる」みたいな感じなので結構判定漏れもあるんですが。 その判定漏れの中で特に深刻なのが、構造体の default を挟んだ場合。

例えば以下のようなコードで、簡単に判定から漏れた null を残せます。

#nullable enable

// これは警告にしてもらえる: 非 null な S に null を渡した。
A a1 = new();
Console.WriteLine(a1.S.Length); // OK

// これだと警告が出ない: default に対する解析がまだない(提案段階)。
A a = default;
Console.WriteLine(a.S.Length); // OK じゃないんだけど OK になる

// S は非 null なはず。
record struct A(string S);

この問題を一番深刻に踏み抜いてるのが、 最近のブログで何度か出て来た ImmutableArray なわけです。

#nullable enable
using System.Collections.Immutable;

var a = new ImmutableArray<int>();

// コードのぱっと見の印象からすると 0 とか返ってきて欲しい。
// 実際にはぬるぽ発生。
// ぬるぽるんだったら、NRT 警告みたいなの出してほしい(これが課題)。
Console.WriteLine(a.Length);

defaultable value type

この問題に対する解決策、方向性としてはシンプルで、 「参照型に対して null を認めないようにフロー解析する」というのと同じノリで、「値型に対して default を認めないようにフロー解析する」というやり方で解決できるはずです。 それが今回説明するdefaultable value type (default 許容値型)。 nullable reference type (null 許容参照型)との対比でこんな名前になっています。

要は、

  • ImmutableArray みたいな型に対して default を渡しているところには警告を出す
  • あえて default を渡したい箇所には、NRT の T? に類する何か(仮に T~ とか書く)みたいなアノテーションを付ける
    • これが defaultable value type

というもの。

nullable と defaultable

ただ、まあ、ちょっとややこしいのが nullable と defaultable があるという点。 C# 2.0 の頃から null 許容値型があるので、 null → default → 有効な値 みたいな「2段の無効な値」ができてしまうという問題があります。

using System.Collections.Immutable;

// null
// Nullable<T>.HasValue で null 判定。
ImmutableArray<int>? a1 = null;
ImmutableArray<int>? a2 = default; // これは null になる

Console.WriteLine(a1.HasValue); // false

// default
// HasValue は true。
// ImmutableArray.IsDefault みたいな別判定が必要。
ImmutableArray<int>? a3 = new();
ImmutableArray<int> a4 = new();
ImmutableArray<int> a5 = default; // これは new() になる

Console.WriteLine(a3.HasValue); // true
Console.WriteLine(a4.IsDefault); // true

// 有効な値
ImmutableArray<int> a6 = ImmutableArray.Create<int>();

Console.WriteLine(a6.IsDefault); // false

これがあるので、defaltable value type に対して T? という記法は使えません。 なので提案では仮に T~ としています。 当初は T?? みたいな案も出ていたんですが、 null 合体演算の ??との弁別が(構文解析が重たくなるという意味で)難しいとのこと。

この仮の ~ を使って話を進めると、とりあえず書きたいコードは以下のようなものになります。

using System.Collections.Immutable;

m1(default); // 警告
m1(ImmutableArray.Create<int>()); // OK
m2(default); // OK

void m1(ImmutableArray<int> a)
{
    // a に default が入ることはなく、a.Length が有効。
    Console.WriteLine(a.Length);
}

void m2(ImmutableArray<int>~ a)
{
    // a に default が入る可能性があり、a.Length のところに警告を出したい。
    Console.WriteLine(a.Length);

    // 非 default を保証するような仕組みも欲しい。
    if (!a.IsDefault)
    {
        Console.WriteLine(a.Length); // これは OK にしたい。
    }
}

参照型フィールドで自動判定

この defaultable value types の最大の目的は ImmutableArray みたいな、内部に参照型フィールドを持っている場合の null 解析です。

なので、

  • 非 null 参照型フィールドを1つでも持っていると「default のまま放置してはいけない型」判定になる
  • 非 null 参照型フィールドをすべて非 null 初期化した時点で「default 状態から脱した」判定になる

という判定を自動的にする予定です。

A a = default;

// 警告: default のまま使った。
Console.WriteLine(a.S);

// OK: S が非 null になった時点で a は非 default。
a.S = "";
Console.WriteLine(a.S);

record struct A(string S);

opt-in

上記の通り、非 null 参照型フィールドを持っている値型は自動的に default 解析の対象になるわけですが、 それ以外の構造体でも「default 放置するとまずい」というものはあります。

例として挙がってるのは "ハンドル" の類ですが、 要は、ポインターに類する値を int とか IntPtr で持っているような構造体。 昔からの習慣で、null と同じく「0 なら無効なハンドル値」とすることが多いです。 こういう型は null 許容参照型とほぼ同じが扱いが必要。

こういう型に対して何らかの新構文を追加すべきか、 それとも属性か何かでアノテーションを付けるかはまだ検討の余地がありますが、 仮に属性を使う案でいうと以下のような感じになります。

[MaybeDefault] // 「default 放置はダメ」を表す何らかの属性
public struct BlobHandle
{
    private readonly nuint _value;

    [AllowDefault] // 「このプロパティが true なら非 default」を表す何らかの属性
    public bool IsNil => _value != 0;

    public byte Read() => // ...
}

void M1(BlobHandle~ handle)
{
    if (!handle.IsNil)
    {
        handle.Read(); // ok
    }
}
M1(default); // ok

void M2(BlobHandle handle)
{
    handle.Read();
}
M2(default); // warning

ちなみに、属性はこれ専用のものを用意すべきか、 それとも null 許容参照型で使っている MaybeNull などの属性をそのまま流用すべきかみたいな点も検討途中です。

default 演算子

前述の IsDefault (ImmutableArray が今持ってるやつ)とか IsNil (前節の例に挙げた BlobHandle のやつ)とかじゃなくて、 default 判定専用の演算子定義も必要なんじゃないかという話もあります。

というのも、以下のようなコード(また ImmutableArray が起こす問題)を考えます。

using System.Collections.Immutable;

void m(ImmutableArray<int> a)
{
    // ImmutableArray に対してリスト パターンを使う。
    // パターンマッチングは暗黙的に非 null 判定を含んでいて、たいていの型に対してはぬるぽを起こさない。
    // ところが…
    Console.WriteLine(a is [1, ..]);
}

// こういうのは大丈夫。
m(ImmutableArray.Create(1)); // true
m(ImmutableArray.Create(2)); // false

// これが例外を起こす。
// null チェックに代わる「default チェック」が必要…
m(default);

こんな感じで「default を放置しちゃダメ」な型に対するパターン マッチングをするにあたって、「null チェック代わりに何か default チェックを挟みたい」という要件があります。

で、「何か特定のプロパティを呼ぶ」とかよりは、以下のように、operator default みたいなものを書けるようにした方がいいのではないかという案も出ています。

public struct ImmutableArray<T>
{
    public static bool operator default(ImmutableArray<T> arr) => arr._array is null;
}

課題

NRT で問題を起こしている以上、defaultable value type みたいなフロー解析が必要なこと自体はもう分かっているわけですが。 話が進まないのはまだ悩ましい点が残っているから。

特に悩ましいとされるのが2点あって、以下のようなものです。

  • プロパティはどうするか
  • NRT 並みに「既存コードの移行作業」に手間が掛かる

プロパティ

record structでは、メンバーは(フィールドではなく)プロパティで作られます。 例えば、record struct A(string S); と書くと、S はプロパティです。

この場合、「すべての非 null 参照型フィールドを初期化していれば非 default」の判定をどうするかという問題があります。 プロパティ S 越しにそのバッキングフィールドを初期化することになるわけですが、プロパティとフィールドの紐づけができないとフロー解析できません。

既存コードの移行

null 許容参照型を導入するときもかなり苦労しました。 .NET の標準ライブラリに null アノテーションを付けて回る作業には2年くらい掛かっています。

しかも、既存コードを壊さないように、「null 解析をするかどうか」は opt-in (明示的にオプション指定しない限り有効化されない)になっていて、「オプションの有無で2種類の C# がある」といってもいいような状況になっています。 (C# チームもこれを好ましいとは思っていないので、 null 許容参照型はそれだけ「無理してでも必要」とされる唯一の機能です。)

defaultable value type ではこの「アノテーション追加」と「opt-in」をもう1度やる必要があります。

Viewing all 483 articles
Browse latest View live