导言
大家使用Python或者Typescript的时候,可能对于它们支持的联合类型比较羡慕,他们写起来象这样:
typescript
let value: string | number;
value = "hello";
value = 123;
是不是很方便? 但是在C#中实现联合类型有更优雅的实现,并且提供强类型检测!
🚀C#中最小联合类型实现
- 新建一个dotnet console项目,使用命令:
dotnet new console -n Test --framework net9.0
- 新建一个类OneOf.cs,在里面实现一个最小的联合类型:
csharp
public struct OneOf<T0, T1>
{
private readonly int _index;
private readonly Object _Value;
public OneOf(T0 value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
_index = 0;
_Value = value;
}
public OneOf(T1 value)
{
_index = 1;
_Value = value!;
}
public static implicit operator OneOf<T0, T1>(T0 value) => new OneOf<T0, T1>(value);
public static implicit operator OneOf<T0, T1>(T1 value) => new OneOf<T0, T1>(value);
public bool IsT0 => _index == 0;
public bool IsT1 => _index == 1;
public T0 AsT0 => IsT0 ? (T0)_Value! : throw new InvalidOperationException($"Cannot return as T0 as result is T{_index}");
public T1 AsT1 => IsT1 ? (T1)_Value! : throw new InvalidOperationException($"Cannot return as T1 as result is T{_index}");
public override string ToString()
{
return _Value?.ToString() ?? "";
}
}
👉 这里的核心思想就是:
- 一个
object
存储实际值 - 一个
int
记录当前类型索引 - 提供隐式转换 + 访问方法
这就是最小化的"联合类型"实现。
- 在Program.cs中测试一下:
csharp
using System.Globalization;
//我们即可以给他赋值为字符型,也可以数值型
OneOf<string, int> test = "hello";
test = 5;
var result1 = Myfunction(true);
Console.WriteLine(result1);
//可以返回字符型,也可以返回数字型
OneOf<string, int> Myfunction(bool flag)
{
if (flag)
{
return "hello";
}
else
{
return 5;
}
}
👉 这样我们就可以把实现的OneOf<T0,T1>联合类型在赋值中、返回值、方法参数中等等使用! 😊怎么样?是不是使用起来很方便?
🎖️"联合类型"进阶
让我们再实现两个辅助方法:Switch与Match OneOf.cs中的OneOf类看起来是这样的:
csharp
public struct OneOf<T0, T1>
{
private readonly int _index;
private readonly Object _Value;
public OneOf(T0 value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
_index = 0;
_Value = value;
}
public OneOf(T1 value)
{
_index = 1;
_Value = value!;
}
public static implicit operator OneOf<T0, T1>(T0 value) => new OneOf<T0, T1>(value);
public static implicit operator OneOf<T0, T1>(T1 value) => new OneOf<T0, T1>(value);
public bool IsT0 => _index == 0;
public bool IsT1 => _index == 1;
public T0 AsT0 => IsT0 ? (T0)_Value! : throw new InvalidOperationException($"Cannot return as T0 as result is T{_index}");
public T1 AsT1 => IsT1 ? (T1)_Value! : throw new InvalidOperationException($"Cannot return as T1 as result is T{_index}");
public override string ToString()
{
return _Value?.ToString() ?? "";
}
/// <summary>
/// 对于没有返回情况的处理
/// </summary>
/// <param name="f0"></param>
/// <param name="f1"></param>
public void Switch(Action<T0> f0, Action<T1> f1)
{
if (_index == 0 && f0 != null)
{
f0((T0)_Value!);
return;
}
if (_index == 1 && f1 != null)
{
f1((T1)_Value!);
return;
}
}
/// <summary>
/// 对于有返回情况的处理
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="f0"></param>
/// <param name="f1"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public TResult Match<TResult>(Func<T0, TResult> f0, Func<T1, TResult> f1)
{
if (_index == 0 && f0 != null)
{
return f0((T0)_Value!);
}
if (_index == 1 && f1 != null)
{
return f1((T1)_Value!);
}
throw new InvalidOperationException();
}
}
这两个辅助方法非常强大,看起来我们使用是这样的:
ini
using System.Globalization;
OneOf<string, int> test = "hello";
test.Switch(
str => Console.WriteLine(str),
num => Console.WriteLine(num.ToString())
);
var result = Myfunction(false);
var result1 = result.Match(
str => str.Length,
num => num * 2
);
Console.WriteLine(result1);
OneOf<string, int> Myfunction(bool flag)
{
if (flag)
{
return "hello";
}
else
{
return 5;
}
}
🚀🚀终极大招(OneOf库)
- 事实上我们上面实现的就是OneOf库的核心代码与核心使用方式;
- OneOf库自动为我们生成
OneOf<T0, ..., Tn>
(最多支持 8 种类型)。 - 生成器(Source Generator)辅助,减少样板代码,可以将范型数量扩展到8个以外。
- 去OneOf库的官网学习: 官方github
👉 OneOf库使用方式
- 通过 NuGet 安装:
csharp
dotnet add package OneOf
- 定义方法参数 using OneOf;
csharp
public class Demo
{
public void Print(OneOf<int, string> value)
{
value.Switch(
i => Console.WriteLine($"这是 int: {i}"),
s => Console.WriteLine($"这是 string: {s}")
);
}
}
使用:
ini
var demo = new Demo();
demo.Print(123);
demo.Print("hello");
Print
方法既能接受 int
又能接受 string
,而且调用时 无需显式转换。
- 作为返回值
csharp
using OneOf;
public class Parser
{
public OneOf<int, string> Parse(string input)
{
if (int.TryParse(input, out var number))
return number; // 隐式转换成 OneOf<int, string>
else
return input; // 也可以返回 string
}
}
调用:
ini
var parser = new Parser();
var result = parser.Parse("123");
result.Switch(
i => Console.WriteLine($"解析成功: {i}"),
s => Console.WriteLine($"解析失败,原始字符串: {s}")
);
如果需要表达式形式,就用 Match
:
csharp
string Describe(OneOf<int, string> value) =>
value.Match(
i => $"数字 {i}",
s => $"文本 '{s}'"
);
Console.WriteLine(Describe(42)); // 输出 "数字 42"
Console.WriteLine(Describe("world")); // 输出 "文本 'world'"