C# 15 の新機能「ユニオン型」
C# 15 の新機能のユニオン型ですが注意があります。それが次の一点。
今は動かない
デフォルトでは今は動かないそれが特徴だけど、諦めないでください。
準備せずユニオン型を書いた場合
次のエラーになります。
error CS0518: 定義済みの型 'System.Runtime.CompilerServices.IUnion' は定義、またはインポートされていません
error CS0656: コンパイラが必要とするメンバー 'System.Runtime.CompilerServices.UnionAttribute..ctor' がありません
これは公式にも書かれていて、今は自分で作る必要があります。こんな感じみたいです。
このコードをあなたのプロジェクトのどこかに含めてください。
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute
{
}
public interface IUnion
{
object? Value { get; }
}
}
ユニオン型
ユニオン型のコードはこんな感じです。
Ku 型の変数に、実態の型を代入できる点が特徴です。
Ku ku1 = new Chiyoda();
Ku ku2 = new Taito();
union Ku(Chiyoda, Taito){ }
record Chiyoda { }
record Taito { }
それ、インターフェイスでは?
いい疑問です。
ほぼ同じことができます。
IKu ku1 = new Chiyoda();
IKu ku2 = new Taito();
interface IKu { }
record Chiyoda : IKu { }
record Taito : IKu { }
しかし、敢えて似て非なるものを作ったということは違う仕様があるはず。これからの時代を作るのは我々ユニオン型だ。そういう気概があるはず。違いを見ていきましょう。
その前に
その前に、ユニオン型に含まれる型で共通のメソッド、どうするのか? 気になっていますね。わかります、私もです。見てみましょう。
インターフェイスの場合
こんな感じですよね。
IKu ku1 = new Chiyoda();
IKu ku2 = new Taito();
_ = ku1.MainStation;
_ = ku2.MainStation;
interface IKu { string MainStation { get; } }
record Chiyoda : IKu { public string MainStation => "Otemachi"; }
record Taito : IKu { public string MainStation => "Ueno"; }
ユニオン型
ユニオン型では、含まれる型の方ではなく、ユニオン型に switch で分岐するコードを実装するそうです。
Ku ku1 = new UnionTypes.Chiyoda();
Ku ku2 = new UnionTypes.Taito();
_ = ku1.MainStation;
_ = ku2.MainStation;
union Ku(Chiyoda, Taito)
{
public string MainStation => this switch
{
Chiyoda c => c.MainStation,
Taito t => t.MainStation
};
}
record Chiyoda { public string MainStation => "Otemachi"; }
record Taito { public string MainStation => "Ueno"; }
何がうれしいのか
実装型をすべて管理できる
インターフェイスは定義後に自由に実装型を増やせます。インターフェイスを定義した人の知らない実装クラスが生まれることになります。
それがインターフェイスのうれしいことでもありますが、場合によっては全てを把握コントロールしたいこともあるでしょう。
ユニオン型は全てを定義者が管理できます。
インターフェイスの場合
interface IKu { }
record Chiyoda : IKu { }
record Taito : IKu { }
record Chuo : IKu { }
ユニオン型の場合
union Ku(Chiyoda, Taito) { }
record Chiyoda { }
record Taito { }
record Chuo { }
switch も完全に把握できる
ユニオン型は全ての実装を把握できるので完璧な過不足のない switch のパターンを作ることができますし、完璧でない場合に警告を出してもらうこともできます。
インターフェイスの場合
次のような分岐があった場合、未知の実装型を想定するしかありません。
public string MainStation => this switch
{
Chiyoda c => c.MainStation,
Taito t => t.MainStation
};
そのため次のような分岐も必要になります。
Chiyoda c => c.MainStation,
Taito t => t.MainStation,
_ => ""
warning CS8509: この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン '_' がカバーされていません。
ユニオン型の場合
型の種類で、分岐の不足を警告してもらえます。次の例では Taito の不足を教えてくれます。
Chiyoda c => c.MainStation,
null=>"null"
この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン 'Taito' がカバーされていません。
全ての型の分岐が書かれると、警告がなくなります。
Chiyoda c => c.MainStation,
Taito t => t.MainStation,
null=>"null"
補足
Ku はクラスなので null があり得ます。そのため次の分岐がないと警告です。
null=>"null"
warning CS8655: この switch 式では一部の null 入力が処理されません (すべてが網羅されているわけではありません)。たとえば、パターン 'null' がカバーされていません。
ユニオン型はユニオン型
ユニオン型の変数の実体は含まれるクラスの型ではなく、ユニオン型です。
Ku ku1 = new Chiyoda();
Console.WriteLine(ku1.GetType().FullName);
そのため型キャストはできません。
Ku ku1 = new Chiyoda();
Ku ku3 = (Chiyoda)ku1;
error CS0030: 型 'Ku' を 'Chiyoda' に変換できません
実体の型にするには Value プロパティがあるので活用します。
Ku ku1 = new Chiyoda();
Chiyoda ku3 = (Chiyoda)ku1.Value;
Console.WriteLine(ku3.GetType().FullName);
パフォーマンス?
世間にはインターフェイスを嫌う人がいます。その嫌う人のパターンの一つがインターフェイスは動作が遅くなる、です。
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1.MainStation;
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface) : {end - start}");
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1 switch
{
Chiyoda c => c.MainStation,
Taito t => t.MainStation
};
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface switch) : {end - start}");
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1.MainStation;
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Union) : {end - start}");
実行結果
Elapsed ticks (Interface) : 88
Elapsed ticks (Interface switch) : 122
Elapsed ticks (Union) : 194
突き詰めた時にどうかは分かりませんが、ただ単純に書き換えることでインターフェイス撲滅! ということはなさそうです。
備えよう
ユニオン型の挙動を見てみました。どこまで使うか、意外と使わなそうな気がしますが、うれしい場面があります。あるものは使っていきましょう。
サンプルコード
今回検証していたコードです。
色々書き換えながら試していたので、皆さんも書き換えながら試してください。
github.com
long times = 1000;
{
Interfaces.IKu ku1 = new Interfaces.Chiyoda();
Interfaces.IKu ku2 = new Interfaces.Taito();
Console.WriteLine($"{ku1.MainStation}, {ku2.MainStation}");
Method(ku1);
Method(ku2);
static void Method(Interfaces.IKu ku)
=> Console.WriteLine(ku switch
{
Interfaces.Chiyoda c => $"Chiyoda: {c.Akihabara}",
Interfaces.Taito t => $"Taito: {t.Asakusa}"
});
Console.WriteLine(ku1.GetType().FullName);
{
long start = DateTime.Now.Ticks;
}
{
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1.MainStation;
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface) : {end - start}");
}
{
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1 switch
{
Interfaces.Chiyoda c => c.MainStation,
Interfaces.Taito t => t.MainStation
};
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface switch) : {end - start}");
}
}
{
UnionTypes.Ku ku1 = new UnionTypes.Chiyoda();
UnionTypes.Ku ku2 = new UnionTypes.Taito();
Console.WriteLine($"{ku1.MainStation}, {ku2.MainStation}");
Method(ku1);
Method(ku2);
static void Method(UnionTypes.Ku ku)
=> Console.WriteLine(ku switch
{
UnionTypes.Chiyoda c => $"Chiyoda: {c.Akihabara}",
UnionTypes.Taito t => $"Taito: {t.Asakusa}"
});
Console.WriteLine(ku1.GetType().FullName);
Console.WriteLine(ku1.Value.GetType().FullName);
try
{
UnionTypes.Chiyoda ku3 = (UnionTypes.Chiyoda)ku1.Value;
Console.WriteLine(ku3.GetType().FullName);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
long start = DateTime.Now.Ticks;
for (long i = 0; i < times; i++)
_ = ku1.MainStation;
long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Union) : {end - start}");
}
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute
{
}
public interface IUnion
{
object? Value { get; }
}
}
namespace Interfaces
{
interface IKu { string MainStation { get; } }
record Chiyoda : IKu
{
public string MainStation => "Otemachi";
public string Akihabara => "Cool!";
}
record Taito : IKu
{
public string MainStation => "Ueno";
public string Asakusa => "Nice!";
}
record Chuo : IKu
{
public string MainStation => "Tokyo";
public string Ginza => "Great!";
}
}
namespace UnionTypes
{
union Ku(Chiyoda, Taito)
{
public string MainStation => this switch
{
Chiyoda c => c.MainStation,
Taito t => t.MainStation,
null => "null"
};
}
record Chiyoda
{
public string MainStation => "Otemachi";
public string Akihabara => "Cool!";
}
record Taito
{
public string MainStation => "Ueno";
public string Asakusa => "Nice!";
}
}