Programming w/ C# ~ 鬼滅の刃コラボ 無限列車編

鬼滅の刃 無限列車編

C#er は、鬼滅の刃 無限列車編 をこう見る。(下記コード部分にはネタバレを含みます。)
NotImplemented はきちんと実装しないと使えませんよ。

using System;
using System.Collections.Generic;
using System.Linq;
using ヒノカミ神楽 = 日の呼吸;

public interface 日の呼吸 {
  public    object? 壱ノ型_円舞()            => throw new NotImplementedException();
  public    object? 弐ノ型_碧羅の天()        => throw new NotImplementedException();
  public    object? 参ノ型_烈日紅鏡()        => throw new NotImplementedException();
  public    object? 肆ノ型_灼骨炎陽()        => throw new NotImplementedException();
  public    object? 伍ノ型_陽華突()          => throw new NotImplementedException();
  public    object? 陸ノ型_日暈の龍_頭舞い() => throw new NotImplementedException();
  public    object? 漆ノ型_斜陽転身()        => throw new NotImplementedException();
  public    object? 㭭ノ型_飛輪陽炎()        => throw new NotImplementedException();
  public    object? 玖ノ型_輝輝恩光()        => throw new NotImplementedException();
  public    object? 拾ノ型_火車()            => throw new NotImplementedException();
  public    object? 拾壱ノ型_幻日虹()        => throw new NotImplementedException();
  public    object? 拾弐ノ型_炎舞()          => throw new NotImplementedException();
  protected object? 拾参ノ型()               => throw new NotImplementedException(); // 後ほど public に修正します
  public    void    回復の呼吸()             { /* ... */ }
  public    void    全集中の呼吸()           { /* ... */ }
  public    void    全集中_常中()            { /* ... */ }
}

public interface 炎の呼吸 : 日の呼吸 { // すべての呼吸は始まりの呼吸の派生 ... のはずなのだが、基底インターフェースのメソッドを実装してないね
  public object? 壱ノ型_不知火()       { /* ... */ } // 無限列車内・対 上弦の参 猗窩座戦で利用
  public object? 弐ノ型_昇り炎天()     { /* ... */ } // 炭治郎防護に利用
  public object? 参ノ型_気炎万象()     { /* ... */ } // 映画/劇場版小説オリジナル
  public object? 肆ノ型_盛炎のうねり() { /* ... */ } // 破壊殺・空式に対応
  public object? 伍ノ型_炎虎()         { /* ... */ } // 破壊殺・乱式に対応
  // 陸・漆・捌ノ型は Obsolete ではないよね ... ?
  public object? 玖ノ型_奥義_煉獄()    { /* ... */ } // 破壊殺・滅式に対応
}

public interface 水の呼吸 : 日の呼吸 {
  public object? 壱ノ型_水面斬り()    { /* ... */ }
  public object? 弐ノ型_水車()        { /* ... */ }
  public object? 参ノ型_流流舞い()    { /* ... */ }
  public object? 肆ノ型_打ち潮()      { /* ... */ }
  public object? 伍ノ型_干天の慈雨()  { /* ... */ }
  public object? 陸ノ型_ねじれ渦()    { /* ... */ }
  public object? 漆ノ型_雫波紋突き()  { /* ... */ }
  public object? 捌ノ型_滝壷()        { /* ... */ }
  public object? 玖ノ型_水流飛沫_乱() { /* ... */ }
  public object? 拾ノ型_生生流転()    { /* ... */ }
  public object? 拾壱ノ型_凪()        => throw new NotImplementedException(); // 義勇さんだけが実装してるよ
}

public interface 雷の呼吸 : 日の呼吸 {
  public object? 壱ノ型_霹靂一閃() { /* ... */ }
  public object? 弐ノ型_稲魂()     { /* ... */ }
  public object? 参ノ型_聚蚊成雷() { /* ... */ }
  public object? 肆ノ型_遠雷()     { /* ... */ }
  public object? 伍ノ型_熱界雷()   { /* ... */ }
  public object? 陸ノ型_電轟雷轟() { /* ... */ }
  public object? 漆ノ型_火雷神()   => throw new NotImplementedException(); // 善逸だけが実装することになるよ
}

public interface 獣の呼吸 : 日の呼吸 { // 風の呼吸に似ているが、派生ではない
  public object? 壱ノ牙_穿ち抜き()      { /* ... */ }
  public object? 弐ノ牙_切り裂き()      { /* ... */ }
  public object? 参ノ牙_喰い裂き()      { /* ... */ }
  public object? 肆ノ牙_切細裂き()      { /* ... */ }
  public object? 伍ノ牙_狂い裂き()      { /* ... */ }
  public object? 陸ノ牙_乱杭咬み()      { /* ... */ }
  public void    漆ノ型_空間識覚()      { /* ... */ }
  public void    捌ノ型_爆裂猛進()      ;
  public object? 玖ノ牙_伸_うねり裂き() ;
  public object? 拾ノ牙_円転旋牙()      ;
}

public interface 血鬼術 {
  public void 再生() { /* ... */ }
}

public interface 爆血 : 血鬼術 {
  public object? 爆血() { /* ... */ }
}

public interface 瑠火の言葉 {
  public void 強く生まれた者の責務を果たす() { /* ... */ } // ここにいる者は誰も死なせない
  public void あとを託す()                   { /* ... */ } // 竈門少年, 猪頭少年, 黄色い少年 ...
}

public interface 柱 {
  public void 後輩の盾となる() { /* ... */ } // 柱ならば後輩の盾となるのは当然だ 柱ならば誰であっても同じことをする 若い芽は摘ませない
}

public class 煉獄杏寿郎 : 炎の呼吸, 柱, 瑠火の言葉 { // 基本に忠実で独自実装がない ... 至高の領域に近い?
}

public class 竈門炭治郎 : ヒノカミ神楽, 水の呼吸 { // 無限列車編 : ヒノカミ神楽のメソッドはまだ 2 つしか実装していないよ
  public    object? 壱ノ型_円舞()            { /* ... */ } // 那田蜘蛛山 対 下弦の伍 累戦で利用
  public    object? 弐ノ型_碧羅の天()        { /* ... */ } // 対 下弦の壱 魘夢戦で利用
  public    object? 参ノ型_烈日紅鏡()        => null; // 実装中 (until 遊郭編)
  public    object? 肆ノ型_灼骨炎陽()        => null; // 実装中 (until 遊郭編)
  public    object? 伍ノ型_陽華突()          => null; // 実装中 (until 刀鍛冶の里編)
  public    object? 陸ノ型_日暈の龍_頭舞い() => null; // 実装中 (until 刀鍛冶の里編)
  public    object? 漆ノ型_斜陽転身()        => null; // 実装中 (until 無限城編)
  public    object? 㭭ノ型_飛輪陽炎()        => null; // 実装中 (until 無限城編)
  public    object? 玖ノ型_輝輝恩光()        => null; // 実装中 (until 無限城編)
  public    object? 拾ノ型_火車()            => null; // 実装中 (until 遊郭編)
  public    object? 拾壱ノ型_幻日虹()        => null; // 実装中 (until 遊郭編)
  public    object? 拾弐ノ型_炎舞()          => null; // 実装中 (until 遊郭編)
  protected object? 拾参ノ型()               => null; // 実装中 (until 無限城編)
  public    object? 円舞一閃()               => null; // 実装中 (until 刀鍛冶の里編)
}

public class 竈門禰豆子 : 爆血 {
  public object? 爪で引き裂く() { /* ... */ }
  public object? 蹴る()         { /* ... */ }
  public void    鬼化進行()     {}            // 実装中 (until 遊郭編)
  public object? 解毒()         => null;      // 実装中 (until 遊郭編)
  public object? 爆血刀()       => null;      // 実装中 (until 刀鍛冶の里編)
  public void    太陽克服()     {}            // 実装中 (until 刀鍛冶の里編)
}

public class 嘴平伊之助 : 獣の呼吸 {
  public void    捌ノ型_爆裂猛進()      {}       // 実装中 (until 遊郭編)
  public object? 玖ノ牙_伸_うねり裂き() => null; // 実装中 (until 無限城編)
  public object? 拾ノ牙_円転旋牙()      => null; // 実装中 (until 無限城編)
  public object? 思いつきの投げ裂き()   => null; // 実装中 (until 無限城編)
}

public class 我妻善逸 : 雷の呼吸 {
  public IEnumerable<object?> 壱ノ型_霹靂一閃_六連() => Enumerable.Repeat((this as 雷の呼吸).壱ノ型_霹靂一閃(), 6);
  public IEnumerable<object?> 壱ノ型_霹靂一閃_八連() => null!;                               // 実装中 (until 遊郭編)
  public object?              壱ノ型_霹靂一閃_神足() => null;                                // 実装中 (until 遊郭編)
  public object?              弐ノ型_稲魂()          => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              参ノ型_聚蚊成雷()      => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              肆ノ型_遠雷()          => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              伍ノ型_熱界雷()        => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              陸ノ型_電轟雷轟()      => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              漆ノ型_火雷神()        => null;                                // 実装中 (until 無限城編)
}

実装中 = 修行により確立
将来予約 = 実戦中に獲得

Programming w/ C# ~ DocFx の利用方法

はじめに

Javajavadoc のように C# においても /// がドキュメントコメントになることは知っており VS Code にもそれ用の拡張機能を加えていた。しかし、これまできちんと運用したことがなかったため、ここらで導入してみることにした。(DoxFx は 2.61.0 時点でまだまだバグがたくさんありそうだ。)
DocFx で Web ページのセットを構築するには、けっこう注意点があるため、ここに記録する。

運用方法

以下のように運用すると非常に便利。

  1. ソースコードとは独立した場所に仕様文書専用フォルダを切る。
  2. すべてのプロジェクトはそのサブフォルダに文書化する。
  3. 各プロジェクトの仕様文書専用フォルダに docfx.json を配置し、そのフォルダを working dir として docfx を起動する doc タスク定義をソースコードの側の .vscode/tasks.json に記述する。
  4. 仕様文書専用フォルダを Docker の Apache で常時 Web 展開する。

VS Code タスク設定

ビルド中間生成物や DocFx のキャッシュなどが悪さをすることがあるため、clean コマンドも整備しておく。

    {
      "label": "doc",
      "type": "shell",
      "command": "docfx",
      "args": [
        "...(仕様文書専用フォルダ)...\\...(プロジェクトサブフォルダ)...\\docfx.json",
        "--cleanupCacheHistory",
      ],
      "group": {
        "kind": "build",
        "isDefault": true,
      },
      "presentation": {
        "reveal": "always",
        "clear": true,
      },
    },
    {
      "label": "clean",
      "type": "shell",
      "command": ".vscode\\_clean.bat",
      "args": [
      ],
      "group": {
        "kind": "build",
        "isDefault": true,
      },
      "presentation": {
        "reveal": "always",
        "clear": true,
      },
    },

クリーンアップバッチは各自の環境におけるビルド出力先 obj フォルダ, bin フォルダに応じて設定する。

@echo off
dotnet clean --nologo
rd /s /q bin
for /f "usebackq tokens=1 delims==" %%d in (`dir /d /b /s ^| findstr /e \\obj ^| findstr /v \\obj\\`) do rd /s /q %%d

名前空間ページの作成

名前空間のページを作成するには namespaces フォルダを新設し、下記のようにアクセスを許可しておく。

    "overwrite": [
      {
        "files": [
          "namespaces/**.md"
        ]
      }
    ]

そして、各名前空間のページを Markdown で記述する。uid には名前空間の識別子を、summary には *content を指定する。

---
uid: UserLib.Common
summary: *content
---
UserLib.Common 名前空間は, 標準の各種オブジェクト型に対する拡張メソッドまたは機能拡張した派生クラスを扱う.

表示抑制の設定

継承メンバーの表示抑制

継承メンバーの表示が煩わしい場面があるため、以下のようにしてノイズ的な情報を表示抑制する。
filterConfig.yml に正規表現を記述して表示抑制する際は docfx --cleanupCacheHistory で文書ビルドしないと正しく反映されないことが多い

継承メンバーの項目全消去 (不採用)

継承メンバーの項目全体を消去するには下記を追記する。docfx の出力文書ではなく css で表示を抑制するようだ。ただ一律消去は弊害が大きいため、不採用とする。

inheritedMembers { display: none; }

Obsolete メンバーの消去

Obsolete メンバーを消去するには、スタイル設定ではなく filterConfig.yml が必要だ。filterConfig.yml は Python のようにインデントにセンシティブであることに注意を要する。

  "build": {
    "filter": "filterConfig.yml",
  }

apiRules:
- exclude:
    hasAttribute:
        uid: System.ObsoleteAttribute

下記のように GitHub の Q&A に従うとメンバーではなく [Obsolete] 属性自体しか消えないため注意。これはこれで属性を表示抑制するときに使えるが。
また uidRegex正規表現であるため、名前空間や引数の揺らぎを吸収するように記述する必要がある。

attributeRules:
- exclude:
    uidRegex: ^System\.Obsolete$

基本クラスのメンバーの消去

System.Object.ToString() などすべてのクラスが継承しているメンバーを Inherited Members に表示されると煩わしいため、消去する。object は SystemObjectobject の間で揺らぎがあることに注意。ここでは Member としているが、Field, Event, Method, Property ごとの指定も可能。(.NET API Docs | docfx 参照)

- exclude:
    uidRegex: ^(System\.)?[Oo]bject
    type: Member

ただし、.Equals(),.HashCode(),.SequenceEquals() などを利用して演算子オーバーロードしている演算系ユーティリティなどでは Inherited Members を消去しない方が望ましいと思われる。object のみならず、独自 Exception を定義している場合は System.Exception のメソッドを消去してもよいかもしれない。

- exclude:
    uidRegex: ^(System\.)?Exception
    type: Method

隠蔽した基底クラス・メソッドの消去

基礎ライブラリのメソッドが充実しすぎているためにオーバーライドして隠蔽したケースなどでは基底クラスのメソッド表示を消去する。

- exclude:
    uidRegex: ^DbExtensions.Database
    type: Member

データコンテナのプロパティの消去

コード付帯のオブジェクトというよりは POCO データコンテナとして用いる record class などは各プロパティの個別説明表示ではなく一覧表として remarks タブ内にリスト表示した方が見やすいため、プロパティ表示を消去する。

- exclude:
    uidRegex: ^DataModel\.(.*\.)?ContainerFormat.+
    type: Property

Markdown 記事の追加

Markdown 記事の追加方法

Markdown で追加記事を記述できる。まずは docfx.json ファイル内 buid > content の第2要素 (メニューバー内容相当) に追加することでリソースアクセスを許可する。

  "build": {
    "content": [
      {
        "files": [
          "reference/**.md",
          "reference/**/toc.yml",
        ]
      }
    ]
  }

次にメニューバーにリンクを設ける。

- name: Reference
  href: reference/

そしてフォルダを切って記事を書く。

### API Reference
- [Ms Docs](https://learn.microsoft.com/ja-jp/dotnet/api/)
- [DbExtensions](https://maxtoroq.github.io/DbExtensions/)

toc.yml は左サイドメニューペインのコンテンツになる。ここで Markdown のページ内参照 (# 以降の部分) の記法は、英文字を小文字に、スペースをハイフンに、アンダースコア以外の記号文字を省略にする必要がある。

- name: API Reference
  href: index.md#api-reference

画像等の追加方法

画像等のファイルは Markdown 記事のフォルダに置いても参照できないようだ。working dir 直下に images フォルダを切ってその中に格納する。

    "resource": [
      {
        "files": [
          "images/**"
        ]
      }
    ]

外部リンク動作の設定

外部リンクは別タブで開く設定を Web ページに施す。そのためにまず javascript の格納場所 styles を作る。注意すべきは docfx.json での記述は格納場所のアクセス権の設定でしかないということだ。ここにスクリプトを置いてもそれだけで Web ページに反映されるわけではない。

 "build": {
    "resource": [
      {
        "files": [
          "styles/*.js",
        ]
      }
    ]
  }

javascript コードを記述する。target="_blank" を使う時は rel="noopener noreferrer" の設定も忘れないこと。

$(document).ready( function () {
  $("a[href^='http']").attr('target', '_blank');
  $("a[href^='http']").attr('rel', 'noopener noreferrer');
} );

最後に、適用する Web ページ (Markdown ファイル) の冒頭に下記の 2 行を追記する。各 Web ページに javascript コントロールのインポートを記述しなければ適用されない。また、google api スクリプトのインポートを 2 つ目のスクリプトに内包して動的呼び出しにしようとすると HTML 評価タイミングの関係で動作しなくなるようで script タグは 2 つ記述する必要がある。

<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="../styles/target_blank.js"></script>

スタイル設定

事前準備としてスタイル用の javascript, css をアクセス権の専用フォルダに配置する。その上でスタイル記述をしたファイルへの参照をスタイル適用するページに追記する必要がある。

  "build": {
    "resource": [
      {
        "files": [
          "styles/*.js",
          "styles/*.css"
        ]
      }
  }

Programming w/ C# ~ Null 安全なジェネリクスのコード記述方法

null 安全なジェネリクス記法

C# 9.0 からようやく null 安全なジェネリクスがまともに記述できるようになったため nullability について試行錯誤してみた。
C# 9.0 以降の nullable コンテクストはそれなりによくできているが、nullablity 解釈の柔軟さ・厳格さに少し揺らぎもあり、複雑な型パズルに出くわして面食らうことがある。

そこで備忘をここに記録する。

結論

  1. 一般に notnull 制限付き T? 記法 (後述 Style 3) を用いるのが望ましい
  2. 任意の型 T を入力として受け入れて、出力にその既定値を返す可能性がある場合 (defaultable) は notnull 制限なし T? 記法 (後述 Style 2) を用いる
  3. 変数の型推論は柔軟 -- 引数はメソッド適用時により広い型の方へ拡大解釈される
    • int 変数は method<T>(T? parameter) {}method<T>(T? parameter) where T : notnull {} に代入可能
    • 変数許容に関して int は null 非許容値型である一方 T? は null 許容型だ、という型相違は警告・エラー報告されない
  4. ラムダ式型推論は厳格 -- ラムダ式シグニチャは厳格に解釈される
    • ラムダ式は入出力の型の揺らぎを一切許容しないため、必要に応じてキャストすること
    • Func<T?, U>Func<T, U> より広いが、キャストなしに後者のインスタンスを前者の変数に代入できない
  5. Source-Typed だけではなく Target-Typed の推論もあるため、notnull 制約を細かく設定していないと拡大解釈されて矛盾が生じることがある
    • 特に Interface 制約 (IComparable<T> など) に注意する必要がある
    • 観点 03 のラムダ式入出力からの型推論と合わさると Tint? と推論されて int? 型は IComparable<int?> 準拠ではない、などのエラーになる可能性がある
  6. キャストには、Target-Typed 型推論が誤って連鎖・波及することがないよう、型推論ドミノをブレイクして調整するストッパーの役割がある

メソッドと制約の記法

null 許容も非許容も struct も class にも広く適用できるジェネリクスを記述しようと思ったら、理論上は下記の 4 スタイルが考えられる。

/// Style 0 -- オーバーロード振り分け記法 (NG)
void Method<T>(T  param) where T : struct {} // null 非許容値型
void Method<T>(T? param) where T : struct {} // null 許容値型
void Method<T>(T  param) where T : class  {} // null 非許容参照型
void Method<T>(T  param) where T : class? {} // null 許容参照型

/// Style 1 -- 制約なし T 記法
void Method<T>(T  param) {}

/// Style 2 -- 制約なし T? 記法
void Method<T>(T? param) {}

/// Style 3 -- notnull 制約付き T? 記法
void method<T>(T? param) where T : notnull {}

Style 0 -- オーバーロード振り分け記法

null 許容 / 非許容 × struct / class のうちの 1 パターンに制限してメソッド定義するならば、この記法が正しい。ただ、複数パターンに広く適用できるジェネリクスには適さない。制約はシグニチャに入らずオーバーロードを許さないことから、メソッドの入口で振り分ける方法は不可能。

Style 1 -- 制約なし T 記法

.NET Core 3.1 (C# 8.0) までの記法。 標準ライブラリも C# 8.0 まではこのように記述されていた。*1

/// C# 8.0
public interface IComparer<in T> {
  public int Compare (T x, T y);
}

nullable enable が既定になった C# 9.0 以降のプロジェクトでは基本的に Style 1 で記述しない方がよいだろう。

Style 2 -- 制約なし T? 記法

.NET 5 (C# 9.0) からの記法。標準ライブラリは C# 9.0 からこのように記述されている。*2

/// C# 9.0 -- Style 2
public interface IComparer<in T> {
  public int Compare (T? x, T? y);
}

しかし、この制約のない ? 付与は、T? が何でも許容することは疑う余地がない一方で、 T 自体が null 許容型を許容するのかしないのか曖昧で解りにくい。

Style 2 と defaultable

そもそも、制約なし型引数 T に対する T? は nullable ではなくて defaultable だという話 *3 がある。

任意の型に対して defaultable で返り値を返したい場合は Style 2 を採用する必要がある。これは入力 T に対して出力として既定値である default(T) を返したいが、T が class の場合の default(T)null になってしまい、型の値域が定義域より広がってしまう (= 入力⊂出力となる) からだ。たとえばこのようなケース。

static TResult? Method<T1, T2, TResult>(this (T1, T2) operands, Func<T1, T2, TResult> func, TResult? defaultValue = default(TResult)) => certainCondition switch {
  true => func(operands.Item1, operands.Item2),
  _    => defaultValue,
};

このケースで注意すべきは、引数初期値を与えている TResult? defaultValue = default(TResult) の部分だ。ここをTResult? defaultValue = default と記述してしまうと TResultclass であれ struct であれ常に null となってしまい、本意とするところと違ってしまう。このメソッドは "出力" 側に TResult? という表記を使用しているが、"入力" (操作したい対象) はあくまで TResult である。既定値を取得すると仮想マシンでは class に対して TResultTResult? の区別がなくどちらも null を返してきてしまうからコード記述でそれを弁別しつつ受け入れるために TResult? という表記をする、というのが defaultable の意味であろう。

Style 3 -- notnull 制約付き T? 記法

そこで考えられるのは notnull 制約を付した Style 3 だ。

LINQ 拡張ライブラリの自作において比較演算評価式をストラテジーパターン的に外挿することが頻出するため、null を取りうる 2 値の比較用に拡張版 .CompareTo() メソッドを記述したが、この際に (プログラムの動作は変わらないものの) スタイルによってプログラマがコード記述に対して意識・懸念することに違いが現れた。Style 3 で記述するとこうなる。

/// Style3 -- nullable 対応版 CompareTo() メソッド
static int CompareToEachOther<T>(T? x, T? y) where T : notnull, IComparable<T> =>
   x?.CompareTo(y) ?? - y?.CompareTo(x) ?? 0;

Style3 で記述するとあることに気付く。xint? 値ではなく int 値を代入するとどうなるか。

答えは、引数 T? x に代入されるときに自然にキャストされ、メソッド内部では int? として扱われるため、 ?.CompareTo()?? も正しくワークする。



Style2 で記述するとどうだろうか。

/// Style2 -- nullable 対応版 CompareTo() メソッド
static int CompareToEachOther<T>(T? x, T? y) where T : IComparable<T> =>
   x?.CompareTo(y) ?? - y?.CompareTo(x) ?? 0;

こちらもほとんど変わり映えしないし、同じように動作はする。が、notnull 制約されていない T 自身に null 許容型が入ったらどうなるかコード記述上の曖昧さが少し気になってしまう。intIComaparable<int> 準拠であるが、int?IComaparable<int?> 準拠ではない。

この null 許容の曖昧さに対する懸念は、単純な例では問題にならないが、後述する Target-Typed 型推論が絡むと大きな混乱を招くリスクとなる。Style 3 にすれば安全だというわけではないが、このリスクを回避するため Style 2 は非推奨だ。



Style 1 で記述するとこうなる。

/// Style1 -- nullable 対応版 CompareTo() メソッド
static int CompareToEachOther<T>(T x, T y) where T : IComparable<T> =>
   x?.CompareTo(y) ?? - y?.CompareTo(x) ?? 0;

x が nullable か否か表明しておらず xstruct が入るかもしれないにも関わらず、不思議なことにこれも正しくワークする。おそらく x?.CompareTo() という表記から x が null 許容型であることを型推論して Style2 と同じ中間コードを生成しているのだろう。しかし、そうであれば Tint? となるはずであり、制約を IComaparable<int> と解釈していることと矛盾する。

こちらはさすがに値型・参照型の違いを弁別せずにやり過ごすのは心情的に難しく、コード表記上 structclass とを分離したくなる。とはいえ、Style 0 のように制約違いオーバーロードによって外形的に分岐させることはできないため、リフレクションを用いたランタイムの型ガードになる。

static int CompareToEachOther<T>(T x, T y) where T : IComparable<T> => typeof(T) switch {
  var type when type.IsClass  => x?.CompareTo(y) ?? - y?.CompareTo(x) ?? 0, // Class
  _                           => x .CompareTo(y),                           // ValueType, Enum
};

元のコードで動作する (というかジェネリクスで生成される中間コードはおそらく同等コードになっているはずな) のに、プログラマがここまで記述するのはさすがに開発効率が悪い。コード表記の整合性を採るべきか否か、元のコードで本当に正しく動作するか、にいちいち悩みたくない。したがって Style 1 は非推奨となる。

ラムダ式型推論と Target-Typed 推論

さて、ここまでの議論であれば、スタイルはいずれでも大した問題にはならずこだわる必要はあまりない。が、スタイルを曖昧にしていると混乱して収拾がつかないことがある。

たとえば、このように IComparable<T> 型を入力とするラムダ式IComparer<T> 実装クラスへラップするコード記述。

/// IComparable<T> 評価ラムダ式を IComparer<T> 準拠に変換するラッパークラス
public class WrapperComparer<T> : IComparer<T> where T : IComparable<T> {
  private Func<T?, T?, int> Comparer { get; set; }

  public WrapperComparer(Func<T?, T?, int>? comparer = null) =>
    this.Comparer =                     comparer ?? ((T? x, T? y) => (x, y).CompareToEachOther());  // for Style 2 or 3

#if __Style_1__
  public WrapperComparer(Func<T , T , int>? comparer = null) =>
    this.Comparer = (Func<T?, T?, int>)(comparer ?? ((T  x, T  y) => (x, y).CompareToEachOther())); // for Style 1 : Func<T, T, int> → Func<T?, T?, int> casting required
#endif

  public int Compare(T? x, T? y) => this.Comparer(x, y);
}

前述の LINQ に比較演算評価式を外挿する場面において、比較演算評価式である IComparable<T>型入力のラムダ式を拡張ライブラリ内では IComparer<T> 準拠クラスへラップする設計としたい。なぜなら、LINQ 標準ライブラリは評価戦略として IComparable<T> 型入力のラムダ式ではなく IComparer<T> 準拠クラスを要求するのが既定となっているからだ。IComparer<T> は応用が利いて悪くないのだが、いちいちクラス定義しなければならず大仰だ。

上記のコードは完成形だが、ここに至るまでに紆余曲折があり、大いに悩まされたことが 1 つある。

当該ラッパークラスは IComparer<T> 準拠であるため、int Compare(T? x, T? y) を実装する必要がある。すなわち Func<T?, T?, int> 型だ。一方これに充て込むために外挿するラムダ式IComparable<T> 準拠のFunc<T, T, int> 型である。

ラッパークラスのコンストラクタに Style 1 の T where T : IComparable<T> 型入力のラムダ式を与えると、IComparable<T> 制約に基づく入力 Func<T, T, int>IComparer<T> 準拠クラスが保持する評価式プロパティ Func<T?, T?, int> にマップする際に nullability の違いが浮き彫りになる。

T 型のインスタンスT? 型の引数やプロパティが受け入れるのとは異なり、Func<T, T, int> 型のインスタンスを、間口がより広いはずの Func<T?, T?, int> 型の引数やプロパティは受け入れない。ラムダ式に対しては、その入出力の型を含めて型の検証が厳格に行われる。

それどころか、Func<T?, T?, int> 型プロパティへ充て込む Func<T, T, int> 型のインスタンスTT? 型だろうとTarget-Typed 推論をし、IComparable<T> 制約へ逆適用してしまう。その結果 int?IComparable<int?>準拠ではないというエラーを発生させる。

Func<T?, T?, int> における T?Tint だとすると T?int? であり、この int? が Target-typed 推論で Func<T, T, int>T にマップされると IComparable<T>IComparable<int?> だということになってしまう。

このギャップを防ぐには Func<T, T, int>Func<T?, T?, int>へと Func の入力型違いをキャストする *4 しかない。そしてこれは、アドホックな回避策などではなく、他の制約と不整合を起こさせないために積極的に打つべき抜本策だ。波及していく型推論ドミノが誤っていたら野火の延焼になる。火元から離れて火事になると原因がわからず大きな混乱となる。キャストには延焼を止めるためのドミノ・ストッパーという重要な役割がある。

Programming w/ C# ~ ステップカウンタ

C# ソースの (フォルダ再帰的な) ステップカウンタ

ソースコードのファイル数・行数を簡便に知りたい。
dir /b /s から find /v /c へ繋いでワンライナー的にやれば ... といつも思って検索をかけるものの、なかなかズバリの解が出てこないため、バッチファイルでステップカウンタを自作した。

@echo off

setlocal enabledelayedexpansion

for /f "usebackq delims=" %%f in (`dir *.cs /b /s ^| findstr /v "\obj"`) do (
  for /f "usebackq delims=" %%c in (`type %%f ^| find /v /c ""`) do (
    set /a LINES+=%%c
    set /a FILES+=1
    set COUNT=     %%c

    echo !COUNT:~-5! %%f
  )
)

set LINES=     !LINES!
echo ---------------------------------------------------------------------------
echo !LINES:~-5! line(s) in !FILES! file(s).

endlocal

dir /b /s コマンドで .cs ファイルの名称をフォルダ再帰で取得し、findstr /v コマンドで \obj フォルダ格納分を対象から除く。
ファイルを1つ1つ find /v /c へ喰わせて行数カウントし LINES 変数へ加算、同時にFILES 変数でファイル数をカウントする。
あとは各ファイルの行数をファイル名、総計を左空白パディングして出力。

実質は 1 重ループであるにも関わらず、コマンド実行結果を変数に割り当てるための 2 つ目の for 文がちょっとイケていない。

C# ソースの (フォルダ再帰的な) 正規表現マッチング

もう一つおまけに、ファイル再帰的に正規表現マッチングするバッチファイルも作ってみた。(文字/改行コード = UTF8/LF 用)

@echo off

setlocal enabledelayedexpansion

set FCNTSUM=0
set LCNTSUM=0
rem DO NOT TRIM BLANK LINES, because the following THREE lines define a variable for an LF character
set LF=^



cls
chcp 65001 >nul

for /f "usebackq delims=" %%f in (`dir *.cs /b /s ^| findstr /v "\obj"`) do (
  set LCNT0=0
  set CONTENT=

  for /f "usebackq delims=" %%c in (`type %%f ^| findstr /n /r /c:%1 %2 %3 %4 %5 %6 %7 %8 %9`) do (
    set /a LCNT0+=1
    set CONTENT=!CONTENT!%%c
    set CONTENT=!CONTENT!!LF!
  )

  if not "!LCNT0!"=="0" (
    set /a FCNTSUM+=1
    set /a LCNTSUM+=!LCNT0!
    set LCNT1=     !LCNT0!
    set LCNT2=!LCNT1:~-5!
    set HEADER=[!LCNT2!][%%f] ------------------------------------------------------------------------------------------

    echo !HEADER:~0,116!
    echo !CONTENT!
    echo.
  )
)

echo ===================================================================================================================
echo !LCNTSUM! line(s) hit in !FCNTSUM! file(s).

endlocal

!CONTENT!%%c!LF! とを結合するのを 2 回に分けるのは %%c の文字列内容に ! が入ると、対象文字列とコンテクストと混同して ! の認識が逆転し、バグり程度がひどくなる (コンテクスト中の !LF!LF が文字列に埋め込まれてしまう) から。

文字列中の !^! へ置換すればよい*1、変数中の文字列置換はこう*2すればよいとのことだが、! のような特殊文字バッチ処理では置換できないようだ。文字列とコンテクストの区別がつかないし、文字列の変換もできないとはなんと出来の悪い言語仕様だ。

バッチファイル書くのしんどい ... 動的型付け言語は好きじゃない。

Programming w/ C# ~ record に対する override method

備忘録

原因不明の不具合に悩まされ、調べたら C# 言語仕様だったということがあったためメモ。

record の override method は class のそれと違う動作をする。

using System;

public class Class1 {
  public string PropC1 = "PropC1";
  public override string ToString() => "Class1";
}

public class Class2 : Class1 {
  public string PropC2 = "PropC2";
}

public record Record1 { 
  public string PropR1 = "PropR1";
  public /* sealed */ override string ToString() => "Record1";
}

public record Record2 : Record1 {
  public string PropR2 = "PropR2";
}

Console.WriteLine($"{new Class1()}");
Console.WriteLine($"{new Class2()}");
Console.WriteLine($"{new Record1()}");
Console.WriteLine($"{new Record2()}");

結果 ... class と record で出力が違う!

Class1                                       // Class1 .ToString() was called.
Class1                                       // Class1 .ToString() was called.
Record1                                      // Record1.ToString() was called.
Record2 { PropR1 = PropR1, PropR2 = PropR2 } // object .ToString() was called! Why?

record の (unsealed) override method を派生 record から呼んだ場合、直近の基底 record の method ではなく、最基底 record (つまり object) の method が適用される。

特に .ToString() はユーザ定義した基底 record の method が呼ばれなくて罠になる。解らなくて半日悩んだ。
record は拡張版 class ではなかったかと ...

このあたりの issue に関連しているか。
[Proposal]: Records with sealed base ToString override · Issue #4174 · dotnet/csharplang · GitHub

Programming w/ C# ~ TypeForwarding

備忘録

あるアセンブリに定義されているクラスを参照させつつ、その実体を別のアセンブリに転送したいと思い、TypeForwarding を調べてみた。

ufcpp.net
learn.microsoft.com

が、なかなかうまくいかない。情報が少なすぎてよくわからなかったが、中国語で書かれているブログ記事を google 翻訳し、そこに書いてあった一言でようやく何が原因か理解した。

TypeForwardedTo 属性は、配置するアセンブリは変更できるが、名前空間は変更できない。
やりたかったことは、アセンブリの配置変更だけではなく (名前空間を跨ぐ) エイリアス付与だった。

global using を使うか、ラッパークラスを定義するしかないか ...

Programming w/ C# ~ Nullable とジェネリクス

備忘録

C# 8.0 から class 型と class? 型が区別されるようになり Null 安全性が高まったが、ジェネリクスでの Nullable の扱いが難しく、特に C# 8.0 と C# 9.0 以降で大きな違いがあるため、後者のスタイルで記述するときのまとめを備忘として記録する。

制約T として受け入れられる型のパターン
structstruct?classclass?
where T : struct ×××
where T : class ×××
where T : class? ××
where T : notnull××
制約なし
指定不可 ××
指定不可 ×

結論としては C# 8.0 を用いる場合は記述環境が不完全なため「nullable enable にできるものの enable にせずに従来型で記述する」のが望ましく、新規プロジェクトでは C# 9.0 以降を用いて Null 安全に記述するのが望ましい。