C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

C#: null れる class と null れない struct いずれの可能性もある object を as<T> したい場合の限界、あるいは C# らしい結論

// これはコンパイルできない(´・ω・`)
// object -> T
static public T As<T>( object o )
{ return o as T; }
// as T できるためには T に class 制約が必要。これはコンパイルできるし、期待動作するかもしれない。
// object(class=nullれる何か) -> T
static public T As<T>( object o )
  where T: class
{ return o as T; }
// これは As<T> が期待動作する。
object a = "Я(やー)";
var aa = As<string>( a );
Console.Write( aa );
// これは As<T> をコンパイルできないから期待動作できない。
object b = 4649;
var bb = As<int>( b ); // <-- oops!
Console.Write( bb );
// これはコンパイルできない(´・ω・`)
static public T? AsNullable<T>( object o )
{ return o as T?; }
// as T? できるためには T に struct 制約が必要。これはコンパイルできるし、期待動作するかもしれない。
static public T? AsNullable<T>( object o )
  where T: struct
{ return o as T?; }
object b = 4649;
var bb = AsNullable<int>( b ); // int? 型になる。
// 暗黙的に int 型のフリが得意な struct をラップした class 的に振る舞う。
Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" );
// null れる
bb = null;
Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" );
// でも(´・ω・`)…
int bbb = bb; // <-- oops! int? は int になれないのでコンパイルエラー。
// 「int? -> int または無かった時の int な補助要員を用意」ならできる
int bbb = bb ?? -1;
// つまり…(´・ω・`) これなら object から int とか struct な T へも As<T> できる。一応。
static public T As<T>( object o, T default_value )
  where T : struct
{ return o as T? ?? default_value; }
// 一応、期待動作する。
object b = 4649;
int bbb = As( b, -1 ); // 型パラメーターは第2実引数から推論できるので省略させられる
// つまり… 2つオーバーロードしておいて…
// 対 class 用 As<T>
static public T As<T>( object o ) where T : class { return o as T; }
// 対 struct 用 As<T>
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// int, float, double, string; string は class, そのほかは struct
var os = new object[]{ 123, 1.23, 12.3f, "hoge" };
// 一応、 どちらにも As<T> できないこともない…(´・ω・`)
foreach ( var o in os )
  Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As<string>( o )}" );
// こんなんが出る。期待動作といえる場合もあるけど、言えない場合もある(´・ω・`)
// 型判定が厳密過ぎて As というより Is Then って感じだね(´・ω・`)
123, -2, -3, 
-1, 1.23, -3, 
-1, -2, 12.3, 
-1, -2, -3, hoge

で、この As<T>As<T> を使う実装者が object の中身が classstruct か知っていないと使えない。そういうわけで「一応」が抜けない(´・ω・`)

// これはダメ、コンパイルエラー。C#のメソッドのシグニチャーに where 句は含まれないので同じシグニチャーのメソッドの重複定義。
static public T As<T>( object o, T default_value ) where T : class { return o as T ?? default_value; }
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// これは期待動作しちゃう。シグニチャーは異なる事になってコンパイルできるし、さっきの os と As<T> の使い方なら期待動作する。
static public T As<T>( object o, object default_value = null ) where T : class { return o as T ?? default_value as T; }
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// でも、こうすると死ぬので「期待動作しちゃう」だった。
var os = new object[]{ 123, 1.23, 12.3f, "hoge" };
foreach ( var o in os )
  Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As( o, "fuga" )}" ); // <-- 最後の As も書き方を統一してみちゃうと死ぬ。

As<T>( o, "fuga" )class 制約の方が呼ばれて欲しいんだけど、そんなプログラマーのお気持ちは言語仕様は組んでゆるふわ動作してくれないから、死ぬ。

// あーれこれと考えた結果、たどり着いた C# の限界に無理をしない As<T> たち
static public T AsClass<T>( object o ) where T : class { return o as T; }
static public T? AsStruct<T>( object o ) where T : struct { return o as T?; }
// ついでにおまけ、(double)34.56 を (int)35 に As りたいとかそーゆー場合用
static public T AsConvertible<T>( object o ) where T : IConvertible { return (T)Convert.ChangeType( o, typeof(T) ); }

まあ…、一応考えてはみましたよ、内部的には AsClass<T>AsStruct<T> も試した結果を (TC,TS?) なタプルで返して…とか、 class X<TC,TS> な感じで classobjectstruct?object をプロパティーで保持しつつ、 operator 群, Equals, GetHashCode, GetType をよしなに実装した薄いラッパーを As<T> で返させて…とか、いっそ実引数で型パラメーター相当の…とか。

でも、 C# の言語仕様の限界をそんな疲れて保守性も悪そうな何かを必死に作ってどうにかせんでも…と思い、そもそもそうなると↑の AsClass, AsStruct, AsConvertible も何かの部品に internal で使うわけでなければ、ほとんど存在価値がないどころか無駄なわけで…。

// 実行時に謎の object をあれかこれかそれか…って処理したいなら(´・ω・`)
void f( object o )
{
  switch ( o )
  {
    case IConvertible convertible: // ... 略
    case MyStructX sx: // ... 略
    case MyClass1 c1: // ... 略
    case MyClass2 c2: // ... 略
  }
}

メソッドでディスパッチしたり包括的に処理するかっこいい何かとかを考えたりするよりも、実際問題、これが一番書くのも保守するのもらく😂

C++er には C#generics の貧弱さが…ツライ😅