.NET中联合类型的实现(OneOf框架核心机理讲解)

导言

大家使用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'"
相关推荐
追逐时光者7 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友8 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧8 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧8 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧8 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang9 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang10 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack10 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis
AAA修煤气灶刘哥11 小时前
面试必问的CAS和ConcurrentHashMap,你搞懂了吗?
后端·面试
我是唐青枫12 小时前
深入掌握 FluentMigrator:C#.NET 数据库迁移框架详解
数据库·c#·.net