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

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

C#: 組み込みの数値型を扱いたい generics の <T> 型と IConvertible の肝っぽい事、それと object 型を暗躍させる必要性のメモ

// 前提; この記事の Fail とか AreEqual とかはこいつの↓
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
// 任意の IConvertible を別の IConvertible に数値を保持して変換したい場合
    public static B C<A, B>( A a )
      where A : IConvertible
      where B : IConvertible
      => (B)Convert.ChangeType( a, typeof( B ) );

↑の単体試験は↓な感じになる。

      // C-1
      AreEqual( 123f, C<decimal, float>( 123m ) );
      // C-2
      try { C<byte, sbyte>( 200 ); }
      catch ( OverflowException ) { }
      catch { Fail(); }

IConvertibleConvert.ChangeType による変換では、

  • 値を保持して変換
  • 変換先で(ほぼ)等価な表現が不可能な場合は例外を飛ばす
    • overflow, underflow になる場合 -> System.OverflowException
    • +∞, -∞, NaN を対応する表現の無い int などへ変換した場合 -> System.OverflowException
  • 変換先に +∞, -∞ がある場合、対応する等価な数値表現が無くても「例外は飛ばない」

てな挙動。最後のやつ、ちょっと怖いね。単体試験にするとこういう事↓

      // C-3
      var a = 1.0e+100d;
      var b = C<double, float>( a );
      AreEqual( float.PositiveInfinity, b );
      AreNotEqual( a, b );

ここまで、 IConvertible の話。

ここから、 object の暗躍が必要な話。

// この実装自体には、意味はないんだけどさ。 object の暗躍が要るの。
    public static T PlusOne<T>( T a )
      where T : IConvertible
    {
      switch ( (object)a )
      {
        case byte v: return (T)(object)(byte)( v + 1 );
        case sbyte v: return (T)(object)(sbyte)( v + 1 );
        case Int16 v: return (T)(object)(Int16)( v + 1 );
        case UInt16 v: return (T)(object)(UInt16)( v + 1 );
        case Int32 v: return (T)(object)(Int32)( v + 1 );
        case UInt32 v: return (T)(object)(UInt32)( v + 1 );
        case Int64 v: return (T)(object)(Int64)( v + 1 );
        case UInt64 v: return (T)(object)(UInt64)( v + 1 );
        case float v: return (T)(object)(float)( v + 1 );
        case double v: return (T)(object)(double)( v + 1 );
        case decimal v: return (T)(object)(decimal)( v + 1 );

      }
      throw new NotImplementedException();
    }

やってる事は簡単、 IConvertible を実装する数値表現の組み込み型なら何でも 1 足しちゃうだけ。これこそまさにジェネリック!って感じだよね。(よく訓練された C#er は「C#ジェネリックはそういう事しない。型ごとちゃんと分けて実装書きましょう💪💪💪」とかたぶん言うんだろーけど。)

とりま、↑コレ、見ての通り、あちこちに object を暗躍させなくちゃならない。 C++er が見たら「テンプレートwwwwwwwとはwwwww」って吹き出したまま衝撃で死んでしまうかもしれない。

// だって C++er これで終わるもん。
template< typename T >
static T Experiment( const T a )
{ return a + 1; }

C++ だと型チェックきちんとしたいってのも C#IConvertible 制約より正確に数値表現の型だけカタカタ(≃テンプレート・メタ・プログラミング)もできるし、実行時に switchobject の型判定して…なんてプログラマーが疲弊する上にも実行時コストもかかるようなエネルギーの無駄遣いをしなくていい。(他にエネルギー使うのではとかそういう事は今回の記事の範囲外😅)

で、既に「じぇねりっくとは・・・」って思いでいっぱいになっているところへ追い打ち。

例えば、 T=byte の場合でも T f()returnbyte を返すためには、 return (T)(object)(byte)(123)object に暗躍してもらわないとならない。

じぇねりっくとは・・・ T ・・・ こんぱいる・・・げんごしょぅ・・・じっこうじこすと・・・

はい、そういうわけで、ジェネリック数値処理ライブラリーは素直に C++ で書いて C# からは P/Invoke というか、P/Invoke 含めて C# でそういうトコは"無茶"して実装しないで、適当なプロセス間通信で適材適所に実装わけた設計にしてプログラマーも実行する電算機さんも楽したらいいんだろうね、って思いました。(´・ω・`)

ちなみに、記事の途中で触れたように、こういうジェネリックの要求がある場合に stackoverflow のイケメン C#er たちは「対応したい型ごと非ジェネリックできっちり書け(▲▲▲▲▲▲▲▲←賛同するイケメンC#erたちのアップヴォートのグレートな山脈が隆起)」みたいになっている様子だったし、わたしもこのC#の仕様の上でもやりたいのなら、それはまあ、 generics は無理しない程度の共通化に使いつつ型ごと実装かかんとちょっとしたループにハマったりするだけで実行時コストが爆発しちゃうだろうなーって思います。でもま、実行時コストが爆発しないところは少々無茶な実装だって、プログラマー=サンのライフがえぐられない方が総合的にみんなハッピーな事も、あるんじゃないかなー、とは思います。

おわり😂