在实际项目中,我们经常遇到这样的场景:有一组策略类,每个策略处理不同的数据类型,但调用方只需要一个统一的非泛型接口来调度它们。问题在于——泛型接口 IFoo<T> 无法直接放进 List<IFoo> 里,因为 IFoo<A>IFoo<B> 是不同的类型。

今天我们从 SwitchData 项目的任务执行器模块出发,看一个优雅的解决方案:泛型接口显式实现(Explicit Interface Implementation)

问题背景

SwitchData 是一个网元数据采集与解析系统,支持多种任务类型(采集、解析、UPF地址解析等),每种任务携带不同的参数数据:

任务类型 参数类 说明
Collect(采集) CollectTaskPayload 无额外参数
Parse(解析) ParseTaskPayload 携带网元组ID或设备ID
ParseUpfAddress ParseUpfAddressTaskPayload 无额外参数

系统需要一个工厂,根据任务类型返回对应的执行器。但每种执行器处理的参数类型不同,如何统一调度?

双层接口设计

核心思路是设计两个接口——一个非泛型接口供工厂和调度层使用,一个泛型接口供具体执行器使用:

// 非泛型接口:工厂返回类型,调度层使用
public interface INeTaskExecutor
{
    NeTaskType TaskType { get; }
    Type PayloadType { get; }
    Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution, object payload,
        PerformContext context, CancellationToken cancellationToken = default);
    string CreateIdempotencyKey(int? taskId, object payload);
    bool TryGetPayload(string payloadJson, out object payload);
}

// 泛型接口:具体执行器使用,获得类型安全
public interface INeTaskExecutor<TPayload> : INeTaskExecutor
{
    Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution, TPayload payload,
        PerformContext context, CancellationToken cancellationToken = default);
}

注意关键区别:

  • 非泛型接口ExecuteAsync 参数是 object payload,所有执行器共享同一签名
  • 泛型接口ExecuteAsync 参数是 TPayload payload,子类获得强类型

两者都叫 ExecuteAsync,参数列表不同,编译器如何区分?

显式接口实现:桥接泛型与非泛型

答案就是 显式接口实现。抽象基类 NeTaskExecutorBase<TPayload> 同时实现两个接口,用显式实现将非泛型方法"隐藏"起来,内部做类型转换后委托给泛型版本:

public abstract class NeTaskExecutorBase<TPayload> : INeTaskExecutor<TPayload>
{
    public abstract NeTaskType TaskType { get; }
    public Type PayloadType => typeof(TPayload);

    // 抽象方法:子类实现,享受强类型 TPayload
    public abstract Task<JsonDataResult> ExecuteAsync(NeTaskExecution execution,
        TPayload payload, PerformContext context, CancellationToken cancellationToken = default);

    public abstract string CreateIdempotencyKey(int? taskId, TPayload payload);

    // 显式实现 INeTaskExecutor(非泛型)—— 桥接层
    async Task<JsonDataResult> INeTaskExecutor.ExecuteAsync(
        NeTaskExecution execution, object payload,
        PerformContext context, CancellationToken cancellationToken)
    {
        return await ExecuteAsync(execution, (TPayload)payload, context, cancellationToken);
    }

    // 显式实现:JSON 反序列化,统一处理
    bool INeTaskExecutor.TryGetPayload(string payloadJson, out object payload)
    {
        try
        {
            payload = JsonHelper.Deserialize<TPayload>(payloadJson);
            return payload != null;
        }
        catch
        {
            payload = default;
            return false;
        }
    }

    // 显式实现:幂等键生成,委托给泛型版本
    string INeTaskExecutor.CreateIdempotencyKey(int? taskId, object payload)
    {
        return CreateIdempotencyKey(taskId, (TPayload)payload);
    }
}

显式实现的三个关键效果

  1. 方法不可见:显式实现的方法无法通过类的实例直接调用,只能通过接口引用访问。这避免了两个重载同时暴露造成的混淆。

  2. 类型转换集中管控object -> TPayload 的强制转换只在显式实现中发生一次,子类永远不需要手动转型。

  3. 通用逻辑复用TryGetPayload 的 JSON 反序列化逻辑在基类统一实现,子类无需关心反序列化细节。

具体执行器:只关心业务

有了基类,具体执行器变得极其简洁,只需关注业务逻辑:

public class CollectTaskExecutor : NeTaskExecutorBase<CollectTaskPayload>
{
    public override NeTaskType TaskType => NeTaskType.Collect;

    public override string CreateIdempotencyKey(int? taskId, CollectTaskPayload payload)
    {
        if (!taskId.HasValue)
            throw new ArgumentNullException(nameof(taskId));
        return $"TaskId={taskId}";
    }

    public override async Task<JsonDataResult> ExecuteAsync(
        NeTaskExecution execution, CollectTaskPayload payload,
        PerformContext context, CancellationToken cancellationToken = default)
    {
        return await NeLogService.CollectLogFilesAsync(cancellationToken);
    }
}

public class ParseTaskExecutor : NeTaskExecutorBase<ParseTaskPayload>
{
    public override NeTaskType TaskType => NeTaskType.Parse;

    public override string CreateIdempotencyKey(int? taskId, ParseTaskPayload payload)
    {
        ArgumentNullException.ThrowIfNull(payload);
        if (taskId.HasValue) return $"TaskId={taskId}";
        if (payload.NeGroupId.HasValue) return $"TaskType={(int)TaskType}:NeGroupId={payload.NeGroupId}";
        if (payload.DeviceId.HasValue) return $"TaskType={(int)TaskType}:DeviceId={payload.DeviceId}";
        throw new InvalidOperationException("网元组ID与设备ID不能同时为空值");
    }

    public override async Task<JsonDataResult> ExecuteAsync(
        NeTaskExecution execution, ParseTaskPayload payload,
        PerformContext context, CancellationToken cancellationToken = default)
    {
        if (payload.NeGroupId.HasValue)
            return await NeLogService.ParseAndPersistGroupAsync(
                payload.NeGroupId.Value, execution.Id, true, cancellationToken);
        else if (payload.DeviceId.HasValue)
            return await NeLogService.ParseAndPersistDeviceAsync(
                payload.DeviceId.Value, execution.Id, true, cancellationToken);
        else
            return new JsonDataResult { success = false, message = "网元组ID与设备ID不能同时为空值" };
    }
}

子类直接使用 ParseTaskPayload,无需任何转型,编译器全程保障类型安全。

工厂:统一注册与分发

工厂利用非泛型接口 INeTaskExecutor 来统一管理所有执行器:

public static class NeTaskExecutorFactory
{
    private static readonly Dictionary<NeTaskType, INeTaskExecutor> _executors;

    static NeTaskExecutorFactory()
    {
        _executors = new Dictionary<NeTaskType, INeTaskExecutor>();

        var executors = new INeTaskExecutor[]
        {
            new CollectTaskExecutor(),
            new ParseTaskExecutor(),
            new ParseUpfAddressTaskExecutor()
        };

        foreach (var executor in executors)
        {
            _executors.Add(executor.TaskType, executor);
        }
    }

    public static INeTaskExecutor GetExecutor(NeTaskType taskType)
    {
        if (_executors.TryGetValue(taskType, out var executor))
            return executor;
        throw new NotSupportedException($"不支持任务类型:{taskType}");
    }
}

这里的关键在于 INeTaskExecutor[]——因为所有执行器都实现了非泛型接口,所以可以放进同一个数组。而调用方拿到的虽然是 INeTaskExecutor(参数是 object),但实际上转型安全,因为显式实现内部做了 (TPayload)payload

调度层:完整的执行流程

在调度入口 NeTaskExecutor 中,整个流程串起来:

private static async Task ExecuteAsync(
    NeTaskExecution execution, PerformContext context, CancellationToken cancellationToken)
{
    execution.StartTime = DateTime.Now;
    try
    {
        // 1. 工厂获取执行器(非泛型接口)
        var executor = NeTaskExecutorFactory.GetExecutor(execution.TaskType);

        // 2. JSON 反序列化为强类型 payload(显式实现统一处理)
        if (!executor.TryGetPayload(execution.PayloadJson, out var payload))
            throw new InvalidOperationException("负载JSON不合法");

        // 3. 执行(内部自动转型为 TPayload,调用子类的泛型方法)
        var result = await executor.ExecuteAsync(execution, payload, context, cancellationToken);

        execution.Status = result.success ? ExecutionStatus.Success : ExecutionStatus.Failed;
        execution.Message = result.message;
    }
    catch (Exception ex)
    {
        execution.Status = ExecutionStatus.Failed;
        execution.Message = ex.Message;
    }
    execution.EndTime = DateTime.Now;
    await NeTaskExecutionService.UpdateAsync(execution, cancellationToken);
}
flowchart TD A[调度层:NeTaskExecutor] --> B[工厂:GetExecutor] B --> C[返回 INeTaskExecutor] C --> D[TryGetPayload:JSON反序列化] D -->|成功| E[ExecuteAsync:object payload] E --> F["显式实现:(TPayload)payload"] F --> G["子类 ExecuteAsync:TPayload payload"] G --> H[具体业务逻辑] D -->|失败| I[抛出异常]

扩展:新增任务类型只需三步

当需要新增一种任务类型时,只需要:

  1. 定义 Payload 类,实现 ITaskPayload
  2. 创建执行器类,继承 NeTaskExecutorBase<YourPayload>
  3. 在工厂的 executors 数组中注册

无需修改调度层、无需修改基类、无需修改任何已有执行器——完美符合开闭原则。

显式实现 vs 隐式实现:何时选择?

特性 隐式实现 显式实现
可通过类实例直接调用 否,必须通过接口引用
可设为 virtual/abstract
适合场景 公开API 桥接层、适配器模式
多接口同名方法冲突 无法区分 可以区分

在本文的场景中,显式实现是唯一选择——因为 INeTaskExecutor.ExecuteAsync(object)INeTaskExecutor<TPayload>.ExecuteAsync(TPayload) 如果都用隐式实现,编译器会报错(参数类型不同不算合法重载,而是同名方法冲突)。

总结

泛型接口的显式实现是一个被低估的 C# 特性,它在以下场景中发挥关键作用:

  • 桥接泛型与非泛型世界:让工厂、调度层等基础设施代码使用 INeTaskExecutor,具体业务代码使用 INeTaskExecutor<TPayload>
  • 集中类型转换object -> T 的转型只在基类的显式实现中出现,子类永远操作强类型
  • 统一通用逻辑:JSON 反序列化、幂等键生成等横切关注点在基类一次性处理
  • 符合开闭原则:新增策略只需继承基类并注册,不改动已有代码

下次当你需要一组处理不同数据类型的策略,又需要统一的调度入口时,试试泛型接口 + 显式实现这个组合。