目次

概要

Ver. 11

C# 11 で、数値型の演算子関連で3つ新機能が追加されています。

背景: Generic Math

C# 11 / .NET 7 でインターフェイスの静的メンバーを仮想・抽象にできる (static abstract members in interfaces)ようになります。 (この機能自体については別ページで別途説明する予定です。)

この機能の一番の用途は、数値型(intfloat など)に対するアルゴリズムをジェネリクスを使って書けるようにすることです。 例えば、以下のようなコードが書けるようになりました。

using System.Numerics;

// よくある「和を取るコード」なものの、
// これまでだとジェネリックに書く手段がなかった。
// C# 11 で可能に。
// (T.Zero や、T に対する + 演算子の定義ができるように)
static T sum<T>(IEnumerable<T> items)
    where T : INumber<T>
{
    var sum = T.Zero;
    foreach (var x in items) sum += x;
    return sum;
}

// いろんな型に対して sum<T> を呼ぶ。
Console.WriteLine(sum(new byte[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(sum(new int[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(sum(new float[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(sum(new double[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(sum(new decimal[] { 1, 2, 3, 4, 5 }));

加減乗除や論理演算はもちろん、float などの一部の型は Math.Sin などの数学関数も使えます。 コンセプト的に、この新機能を使ったジェネリックな数値処理の事を Generic Math と呼んでいたりします。

また、 .NET 5 以降、数値関連の型がいくつか追加されています。

  • Half: 16ビット浮動小数点数
  • Int128, UInt128: 128ビットの整数
  • CLong, CULong: C/C++ との相互運用のために使う、環境によってビット幅が違う整数
  • nint, nuint: CPU 依存幅の整数

これらの新しい数値型も、Generic Math の対象で、INumeric<T> などのインターフェイスを実装しています。

この Generic Math と関連して、数値型の演算子関連で細々とした機能がいくつか追加されています。

  • 符号なし右シフト
  • checked 演算子オーバーロード
  • シフト演算子の右オペランドの制限撤廃

符号なし右シフト

右シフト演算には符号付き右シフト(算術シフト)と符号なし右シフト(論理シフト)があって、 右シフトしたときに、最上位ビットの 1 が残るかどうかの差になります。

C# の場合、基本的に、

  • 符号付き整数の右シフトは符号付き右シフト(算術シフト)
  • 符号なし整数の右シフトは符号なし右シフト(論理シフト)

という方式で右シフトの方式を切り替えます。

// 符号なし (unsigned) の 0xFF = 255
byte u = 0xFF;

// 符号付き (signed) の 0xFF = -1
sbyte s = (sbyte)u;

// 符号なしを右シフトすると、左端には 0 が入る。
// FF → 7F → 3F → 1F → F → 7 → 3 → 1
for (int i = 0; i < 8; i++)
{
    Console.WriteLine($"{u:X}");
    u >>= 1;
}

// 符号なしを右シフトすると、左端のビットが残る。
// 元が FF だとずっと FF。
for (int i = 0; i < 8; i++)
{
    Console.WriteLine($"{s:X}");
    s >>= 1;
}

右シフトの符号あり/なしを切り替えたい場合、キャストが必要でした。

sbyte s = -1;

// LogicalRightShift を呼んでいるので、符号なし右シフトになる。
// FF → 7F → 3F → 1F → F → 7 → 3 → 1
for (int i = 0; i < 8; i++)
{
    Console.WriteLine($"{s:X}");
    s = LogicalRightShift(s, 1);
}

// 右シフトの符号のあり/なしを切り替えたい場合、キャストを挟む。
static sbyte LogicalRightShift(sbyte s, int bits)
    => (sbyte)((byte)s >> bits);

この方式は、Generic Math の導入に伴って1つ問題がありました。 「型引数 T に対応する符号なしな型」を取得する手段がありません。

// 符号なしシフトにしたかったらどうすれば???
static T LogicalRightShift<T>(T s, int bits)
    where T : IShiftOperators<T,T>
    => (T)((/* unsigned T を取得したいけど手段がない */)s >> bits);

そこで、C# 11 では普通に「符号なし右シフト演算子」の >>> (> 3つ)を導入することにしました。 (Java にあるやつです。Java の場合は uint などの符号なし整数型がなくて、>>>>> で右シフトを切り替えます。)

using System.Numerics;

sbyte s = -1;

// ちゃんと符号なし右シフトに。
// FF → 7F → 3F → 1F → F → 7 → 3 → 1
for (int i = 0; i < 8; i++)
{
    Console.WriteLine($"{s:X}");
    s = LogicalRightShift(s, 1);
}

// >>> でどの型に対しても符号なし右シフト。
static T LogicalRightShift<T>(T s, int bits)
    where T : IShiftOperators<T,T>
    => s >>> bits;

ちなみに、演算子オーバーロードもできます。

for (int i = 0; i < 4; i++)
{
    var x = new Int2Bit(i);

    Console.WriteLine($"for {x}");

    for (int j = 0; j <= 2; j++)
    {
        Console.WriteLine($"{j} bit signed: {x >> j}, unsigned: {x >>> j}");
    }
}

readonly struct Int2Bit
{
    public readonly byte Value;
    public Int2Bit(int value) => Value = (byte)(value & 0b11);
    public override string ToString() => Value.ToString();

    public static Int2Bit operator >>(Int2Bit x, int y) => new(x.Value >> y);
    public static Int2Bit operator >>>(Int2Bit x, int y) => new(ExtendSign(x.Value) >> y);
    private static int ExtendSign(int x) => x is >= 0b10 ? (-4 | x) : x;
}

checked 演算子オーバーロード

(書きかけ)

シフト演算子の右オペランドの制限撤廃

(書きかけ)

更新履歴

ブログ