【问题标题】:How to overload and return multiple types with a C# async method?如何使用 C# 异步方法重载和返回多种类型?
【发布时间】:2018-07-13 11:19:31
【问题描述】:

我想要一堆 GetDataAsync() 方法,这些方法被重载以根据所需类型返回各种数据集。如果函数不是异步的,我可以根据out 参数重载:

public void GetData(int userId, out int age, out string name) { ... }
public void GetData(int userId, out string name, out string address) { ... }

但是out 不允许用于async 方法。接下来,我尝试使用泛型来重载(我将stringint 更改为其他类型,因为这个技巧对密封类无效):

public async Task<Tuple<TName, TAddress>> GetDataAsync<TName, TAddress>(int userId)
    where TAddress : Address where TName : Name
{ ... }
public async Task<Tuple<TAge, TName>> GetDataAsync<TAge, TName>(int userId)
    where TAge : Age where TName : Name
{ ... }

但我认为模板专业化在 C# 中并不存在,因此这些被认为是相同的功能。

确实启用此重载的丑陋技巧是传入函数填充的缓冲区:

public async Task GetDataAsync(int userId, int[] id_output, string[] name_output)
{
    await Task.Delay(10);
    id_output[0] = 60;
    name_output[0] = "Freddy";
}

public async Task GetDataAsync(int userId, string[] name_output, string[] address_output)
{
    await Task.Delay(10);
    name_output[0] = "Freddy";
    address_output[0] = "Elm Street";
}

但是那个 hack 有一个糟糕的 API。 (使用列表作为缓冲区会更安全一些。)是否有任何适当的 C# 方法可以从异步重载中获取不同的数据类型?如果必须,我会将数据类型合并到函数名称中而不是重载,但这似乎是不必要的冗长。而且由于函数抽象了数据库调用,其中一些函数的名称确实很长! (这些函数大多有不止一个调用者,因此希望集中定义它们,而不是在调用它们的类中。)

【问题讨论】:

  • 我不认为GetData 是一个很好的名字。不是每个提供某种形式的输出的方法都会获取数据吗?他们真的有如此密切的关系,以至于他们应该有相同的名字吗?在我看来,他们似乎没有。
  • @Damien_The_Unbeliever 每个方法从数据库中获取两条或更多条数据(单行的一部分),并初始化每条数据(通常是 JSON 的反序列化)。在我看来,这些方法大体相似。
  • @piojo - 但可能不适用于获得您的代码并被告知使用它的人。太笼统了。他们肯定有更具体的目的。
  • @Sinatr 一个函数不允许只通过返回类型来区分,否则这不会是一个问题。
  • 在编译时选择重载对您来说有多重要?如果您有一个(公共)通用方法,然后在执行时填充正确的值,那是否足够好?如果您使用不受支持的组合,则不会出现编译时错误,但否则它会起作用。我会把它写成答案,但是写起来需要一段时间,如果编译时验证是必须的,我不想浪费时间。 (如果只有 您的 代码会调用它,并且都经过单元测试,那可能不是问题。)

标签: c# async-await overloading


【解决方案1】:

out 参数是为返回多个值而创建的,但它们不适用于异步方法,因为真正的方法在概念方法结束执行之前返回。

这就是为什么tuples 被添加到语言中的原因。

元组表示一组松散相关的值。它们并非旨在代表数据库原始、实体或任何具有其自身含义/概念的事物。

数组也不是一个好的选择,因为您不能保证将数组作为方法参数一无所获,而且出于此目的,它们根本不是强类型的。

您似乎在寻找的是generics

Task<Response> GetAsync<Request, Response>(TRequest request);

【讨论】:

  • 不幸的是,如果TRequest 不是唯一类型(例如,用户名和身份验证令牌),则此方法不起作用。但是如果我可以将返回类型信息填充到TRequest 中,例如Request&lt;PlayerInventory, BattleData&gt;,它可能会起作用。
  • 当我这样做时,我创建了请求/响应对。通常 Response 类型派生自基类,目的与您提到的完全一样。
【解决方案2】:

类型安全且相对干净的解决方案的灵感来自 Paulo Morgado 的回答。 Request 类包装了以前的参数,这些参数不能用于重载,因为它们总是相同的——用户 ID 和令牌。在编译器看来,未使用的类型参数仅用于区分 Request&lt;Foo, Bar&gt;Request&lt;Foo, Quux&gt;

public class Request<T1, T2>
{
    // Common data needed by every GetAsync(), no matter what it returns:
    public string user;
    public string token;

    public Request(string user, string token)
    {
        this.user = user;
        this.token = token;
    }
}

public class Request<T1, T2, T3> : Request<T1, T2>
{
    public Request(string user, string token) : base(user, token) { }
}

然后可以重载这些方法,并且可以使每个方法返回与请求对应的类型。 (函数必须手动编写,但它们的使用是类型安全的。)

public async Task<Tuple<UserInventory, UserHeroSet>> LoadAsync(
    Request<ServerUserInventory, UserHeroSet> request)
{ ... }

public async Task<Tuple<UserInventory, BattleData>> LoadAsync(
    Request<UserInventory, BattleData> request)
{ ... }

使用方法如下:

var dbTuple = await LoadAsync(new Request<UserInventory, BattleData>(user, token)).ConfigureAwait(false);
dbTuple.Deconstruct(out UserInventory inventory, out BattleData battleData);

【讨论】:

    猜你喜欢
    • 2019-04-05
    • 2019-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-12
    • 1970-01-01
    相关资源
    最近更新 更多