.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'"
相关推荐
界面开发小八哥3 小时前
界面控件DevExpress WPF中文教程:Data Grid - 绑定数据
ui·.net·wpf·界面控件·devexpress·ui开发
毅航4 小时前
从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑
后端·面试·mybatis
展信佳_daydayup4 小时前
02 基础篇-OpenHarmony 的编译工具
后端·面试·编译器
Always_Passion4 小时前
二、开发一个简单的MCP Server
后端
用户721522078774 小时前
基于LD_PRELOAD的命令行参数安全混淆技术
后端
笃行3504 小时前
开源大模型实战:GPT-OSS本地部署与全面测评
后端
知其然亦知其所以然4 小时前
SpringAI:Mistral AI 聊天?一文带你跑通!
后端·spring·openai
庚云4 小时前
🔒 前后端 AES 加密解密实战(Vue3 + Node.js)
前端·后端
超级小忍5 小时前
使用 GraalVM Native Image 将 Spring Boot 应用编译为跨平台原生镜像:完整指南
java·spring boot·后端
倔强的石头6 小时前
Mihomo party如何在linux上使用
后端