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

nameof(T)

$
0
0

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

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

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

今日は最後の1個の nameof(T<>) の話です。 当初「3つまとめて1ブログにする予定」だった原因。 こいつだけ対して書くことがなく…

nameof(T<>)

今日のやつは Visual Studio 17.13.0 Preview 2 (.NET 9 の正式リリースの次のアプデ)で merge 済みです。 nameof 演算子の中に unbound な型を書けるようになりました。 unbound (未束縛)というのは、List<> みたいに、型実引数を渡してなくて(<> の中に何も書かず)、具体的な型が決定していない状態のジェネリック型のことを言います。

例えば以下のような感じ。

var name = nameof(List<>.Count);

Console.WriteLine(name); // Count

元々、nameof(T<int>) とか書いても、結果の文字列は T だけで、型引数は何にも影響しません。 メンバー参照でも、nameof(T.X) でも nameof(T<int>.X) でも nameof(T<string>.X) でも、得られる文字列は X です。 つまり、nameof に取って型引数は全くの無意味でした。

それでもこれまでは unbound な型は掛けず、何か適当なダミーの型実引数を渡す必要がありました。 上記の例であれば、適当に object なり int なりを渡して、 nameof(List<int>.Count) とか書いていました。

typeof の場合は typeof(T<>) (unbound な型の Type 型インスタンスが取れる)とか書けるわけで、 nameof でも nameof(T<>) と書けてもいいじゃないかと前々から言われていました。

まあ、別に特に問題があってできなかったわけではなくて「それなりに実装コストがかかるから後回し」みたいな感じで放置されていただけです。 typeof(T<>) と共通のコードでできそうに見えるかもしれませんが、 typeof(T<>) の方では typeof(T<>.X) とメンバー参照することはないので、 nameof では「似て非なるものの再実装」が必要とのことです。

// unbound でメンバー参照(特にインスタンス メンバーの参照)をするのは nameof だけ。
var name = nameof(List<>.Count);

// 入れ子の型なら参照することはあるけども、
List<int>.Enumerator e1 = default;

// unbound はあり得ない。
List<>.Enumerator e2 = default;

// まして、インスタンス メンバー参照はあり得ない。
_ = List<>.Count;

// 入れ子の型は unboud な typeof ができるけど、
var t1 = typeof(List<>.Enumerator);

// メンバー参照はあり得ない。
var m1 = typeof(List<>.Count);

一応、「理由なく掛かっていた制限を取り払った」以上の意味もありまして、 これまでは「型制約の関係でどうやっても nameof を使いにくい」という場面がありえました。 一例として、以下のような場面があり得ます。

var name1 = nameof(A<_>); // これは書けるけど、
var name2 = nameof(B<_>); // これは書けない。


// 「無意味な nameof 型引数のためのダミーはこの型を使う」みたいな規約でやってたとして…
// 型制約によっては規約を守れない。
class _;

class A<T> where T : class;
class B<T> where T : struct;

この例はまだ「規約が守れない」程度の話ですが、 型制約が複雑になるにつれ、「そもそも nameof が使えない」みたいなことも起こりえるそうです。

とうことで、優先度は低くて放置はされていたものの、ようやく unbound な nameof(T<>) を認める実装が merge されました。

おまけ: typeof がらみを定数扱いする特殊処理

おまけでもう1個似たような話。

nameof から取れる名前はかなり限られています。 nameof(T<Arg1, Arg2, Arg3>) から取れるのは T だけですし、 nameof(A.B.C.D<E, F>.G) から取れるのは G だけです。

これに対して、

  • フルネームを取りたい
  • 型引数も含めて取りたい

みたいなこともなくはないらしく。 一時は fullnameof みたいな提案も出たことがあるくらいです。

これに対する解決案として、typeof で取った Type 型のプロパティ NameFullName を特殊処理で定数扱いしてはどうか?というものも一瞬提案されたりしてました。

まあ、余りにもニッチで役立つ場面が少なすぎるということでリジェクトされて終わりましたが…


文字列リテラルを data セクションに UTF-8 で書き込む案

$
0
0

ここ数回のブログ( その1その2その3 )、Language Feature Status に最近かかった更新のうち、すでに実装されたものの紹介をしていたわけですが。

「その Language Feature Status を見てると、何やら見慣れないものもちらほら増えてない?」みたいな話題も出ておりまして、今回からその辺りに触れていきたいと思います。

今日は「String literals in data section as UTF8」というやつを。

概要

普通にやると、C# の文字列リテラルや定数はアセンブリ(exe や dll)中の UserString セクションというところに UTF-16 で記録されます。 今回取り上げる話は、オプション追加で、特定の条件を満たす文字列は data セクションというところに UTF-8 で記録できるようにしたいという話です。

おおむね、例えば以下のコードを、

string s = "Some very very long looo...ooong string!!!!!";

特定条件下では以下のようなコード扱いでコンパイルするというもの。

string s = System.Text.Encoding.UTF8.GetString(
    "Some very very long looo...ooong string!!!!!"u8
    );
// ※ 実際にはこの GetString 結果を静的にキャッシュ

ちなみにこれから説明していきますが、一番の目的は「data セクションに書くこと」で、 UTF-8 化(その結果、dll サイズが大体縮む)は副産物だそうです。

24ビット制限

PE ファイル(.NET の exe とか dll のファイル形式)中のメタデータの仕様上の問題なんですが、 C# の文字列リテラルって合計サイズで24ビット長(約 1,600万バイト、800万文字(以下 8M と表記))を超えれないそうです。

どうも理屈としては、

  • 文字列リテラルは UserString セクションというところに書き込む
  • UserString セクション中の文字列を表すハンドル値は4バイト
  • そのうち1バイトは「テーブル番号」
  • 残り3バイト(24ビット)が「文字列の先頭までのオフセット(バイト数)」
  • 文字列の合計長が 16M バイト(8M 文字)を超えると、その次の文字列のオフセットが24ビットを超えてオーバーフロー

とのこと。

なので、これを超えると C# コンパイラーも CS8103 エラーを出すんですって。

しかも「8M を超えたその次の文字列リテラルがあるとダメ」というのが曲者で、 「一番最後のファイルの、一番後ろのメソッド中の、一番最後の文字列リテラルがでかい」という状況だと合計サイズ 8M 超でもコンパイルできちゃうそうです(最後の1個だけはセーフ)。 その状況で、「それより後ろに1個、短かろうが別の文字列リテラルが増えた」となると突然にアウト。

制限超過

まあ、8M 文字よ? 原稿用紙2万枚(日本人にしか通じない単位)。 太宰治の「人間失格」が8万文字くらいっぽいので、それ100冊分。 皆様方、「1プロジェクトでそんなの超えるやついるの?」と疑うかと思われます。

そう疑う方はぜひ、「CS8103」で Web 検索してみてください。 案外たくさん踏んでるやついる…

特によく出てくる話だと、razor ファイルがビルド時に文字列化されて、UserString セクションに書き込まれるせいっぽいです。 でかい ASP.NET プロジェクトだとそこそこ 8M を超えてしまうとか。 (まあこれに関しては ASP.NET 側が「.razor, .cshtml が生成する文字列、u8 リテラルに変えたら?」とか言われたりもしますが。)

他にも、「大量の SQL を文字列で埋め込んでる」とか「元々あったでかい CSV ファイルの中身を文字列リテラルで埋め込んだら起きた」とか、 中々に「ほー、それで 8M 行っちゃったかー」と関心()するようなコメントも多数。

data セクションと UTF-8 リテラル

ところで、C# 11 のときに UTF-8 リテラルというものを導入したわけですが、こいつは単なる定数バイト列にコンパイルされます。 で、定数バイト列は data セクションってところに記録されるそうです。

そして、data セクション(というか、UserString セクション以外のもの大体)は制限サイズが29ビットだそうで。 UserString よりも5ビット多く、32倍! 256M!

しかも、問題を起こしがちなのが .razor とかということは、 多くの部分がマークアップとかスクリプトですからね。 大体 ASCII 文字。 ASCII 文字は、UTF-8 だと1バイト、UTF-16 だと2バイト。 つまり、UTF-8 にするとそれだけで半分のサイズ。 さらに倍(合わせて64倍)の文字数を記録できるはず!512M!

64倍程度の差でどのくらい「突然 CS8103 が出た!」という「バグ報告」(自称)が減るでしょうか… ムーアの法則的に言うと10年くらいの延命にはなりますかね。

まあ、 .NET ができた当初、 こんなさらっと 8M 文字超えてくるような使われ方も、 バイナリデータ用のセクションに UTF-8 を書き込んで使うやり方も全く想像してなかったでしょうね…

コンパイラー オプションでやってみる

という話の流れで最初の話題になります。 「1回、試しに C# コンパイラー側でやってみる?」と。 長い文字列を見たら u8 リテラル化して、読み込み時に UTF8.GetString を挟む実装。

これ、あくまで「C# コンパイラー実装」の話であって、 C# 言語の文法上は何一つ影響がないので Roslyn 側にしか作業はありません。 (csharplang 上に関連項目なし。)

そして、まあ、問題起こすのの大部分が ASP.NET だし、 先ほどちょっと触れていますが ASP.NET 側がコード生成方針変えろという話もあるので、 本当に C# コンパイラー側でやる価値があるのかどうかがちょっと自信なさげな感じではあります。

なのでとりあえず experimental。 「いったん merge されたものが世に出荷された上で、やっぱり取りやめになって消える」みたいなことがあり得る状態なのでご注意ください。

軽く現状の仕様を紹介しておくと以下のような感じ。

  • UTF8 リテラルでやってることを流用するので実装はそんなに難しくはない
  • feature フラグ (/feature オプションに渡すフラグ名)として experimental-data-section-string-literals を用意
    • experimental-data-section-string-literals=20 みたいに最後に「閾値の文字数」を渡す
    • この文字数を超えた文字列リテラルだけが対象 (=0 と書けばすべての文字列リテラルが対象)
  • パフォーマンスはそこまで悪くならない予想(多少は不利)
    • 静的にキャッシュするので GetString は初回のみ
    • しかも最近、UTF8.GetString のパフォーマンスは JIT 最適化かかってる
    • ldstr (定数文字列のロード)と ldsfld (キャッシュした静的フィールドのロード)の命令自体はそんなにパフォーマンス差がない
    • ただ、定数文字列は文字列専用のヒープに書き込まれたり、JIT 時最適化はかかりやすくて、そこは GetString を挟むのが不利
    • 静的キャッシュを持つための匿名型が作られるので、型ロードのコストはかかる

まとめ

というか感想。 「そんなやついるんだ」からの、「そんな対処するんだ」。

まあ「副作用として UTF-8 化すると文字列データのサイズがほぼ半分」の方が結構ありがたい可能性があります。 dll のバイナリサイズが問題になることもちらほらありますしね。 UTF-8 だと日本語なんかは逆にサイズが増えるんですが、 案外、プログラム中に埋め込んでる文字列リテラルは ASCII なもので (razor 同様マークアップだったりとか、URL だったりとか、ハッシュ値の base64 だったりとか)。

ファイナライザー

$
0
0

以下の ~Class1 のこと、(C# の言語機能名として)なんと呼びますか?

class Class1
{
    public Class1() { }
    ~Class1() { }
}

すごく今更ながら、8年くらい前からこれの呼び名が変わってたらしいというのを最近気づいたという話になります。

ちなみに結果だけ言うと、旧称がデストラクター(destructor)、今はファイナライザー(finalizer)です。

他の言語とかの話

C# がかつてこいつのことをデストラクターと読んでいたのは C++ 由来です。 ただ…

  • 文法は確かに同じで、C++ も ~Class1 な文法がある
    • これのことをデストラクターと呼ぶ
    • ただし、挙動は C# のものと違っていて、呼ばれるタイミングは「変数のスコープを抜けるとき」
  • C# は文法こそ同じなものの、実際にはそれはファイナライザーだった
    • Java の finalize メソッド(ファイナライザーって呼ばれてる)と同じ挙動
    • GC 回収されるタイミングで呼ばれる
    • C# もコンパイル結果的には object.Finalize メソッドのオーバーロードとして実装
    • GC.SuppressFinalize って名前のメソッドもある
    • .NET の仕様書上も呼び名はファイナライザー

文法だけ同じな C++ 由来の名前(デストラクター)で、意味合いとしては違うもの(ファイナライザー)という地雷。

しかも、「.NET の仕様書はファイナライザーと呼んでいるものを、C# のレベルではデストラクターと呼んでいた」という2個目の地雷。

Deconstruct との兼ね合い

さかのぼること2016年末、うちのブログでも書いてましたね、これの話。

ここにある通り、発端は C# 7 の頃(20217年3月頃正式リリース)に導入した分解構文でして。 「Deconstruct って英単語は微妙じゃない?」という話から派生して、 「誰だよ、~Class1 のことをデストラクターって言ったの」という話題にまで及びまして。

このブログの最後で「デストラクターって呼び名は微妙なので変えることも視野に入ってるみたい」と書いていますが、 実際、その後変更されたみたいですね。 Microsoft Learn (前記のブログの時代は MSDN だったもの)の文章の大元になってる dotnet/docs リポジトリを見てみた感じ、 割と早い段階から finalizer に置き換わっていました。

ということで直します

前述のブログの話があったので長いこと自分も口頭ではファイナライザーって呼んでたりはしたんですが。 Learn が案外ちゃんとドキュメントの修正を早期に入れていたことには気づいておらず。 気が付いたら8年くらい放置してましたね…

うちのサイトも長らく放置で各所にデストラクターと書いたままなんで、直します… (すぐにやるとは言っていない。)

Viewing all 483 articles
Browse latest View live