代码之家  ›  专栏  ›  技术社区  ›  Mark

一个i防止使用通用限制的特定类型

  •  6
  • Mark  · 技术社区  · 15 年前

    我有一个重载方法——第一个实现总是返回一个对象,第二个实现总是返回一个枚举。

    我想使这些方法成为通用的 重载,并限制编译器在泛型类型可枚举时尝试绑定到非枚举方法…

    class Cache
    {
        T GetOrAdd<T> (string cachekey, Func<T> fnGetItem)
            where T : {is not IEnumerable}
        {
        }
    
        T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem)
        {
        }
    }
    

    用于…

    {
        // The compile should choose the 1st overload
        var customer = Cache.GetOrAdd("FirstCustomer", () => context.Customers.First());
    
        // The compile should choose the 2nd overload
        var customers = Cache.GetOrAdd("AllCustomers", () => context.Customers.ToArray());
    }
    

    这仅仅是我在这里侵犯的一种明显的坏代码味道,还是有可能消除上述方法的歧义,以便编译器总是正确地获取调用代码?

    为任何能给出除“重命名方法之一”以外的任何答案的人投票。

    4 回复  |  直到 15 年前
        1
  •  2
  •   James Dunne    15 年前

    只使用一种方法并让它检测 IEnumerable<T> 动态案例,而不是通过一般约束尝试不可能的案例。根据要存储/检索的对象是否可枚举,必须处理两种不同的缓存方法是“代码味道”。而且,仅仅因为它实现了 IEnumerable<t> 并不意味着它必然是一个集合。

        2
  •  8
  •   Eric Lippert    15 年前

    重命名其中一个方法。你会注意到的 List<T> 有一个add-and-addrange方法;遵循该模式。对一个项目做一些事情和对一系列项目做一些事情在逻辑上是不同的任务,所以使方法具有不同的名称。

        3
  •  5
  •   LBushkin    15 年前

    这是一个很难支持的用例,因为C编译器如何执行重载解析,以及它如何决定要绑定到哪个方法。

    第一个问题是 constraints are not part of the signature 不考虑过载分辨率。

    您必须克服的第二个问题是,编译器从可用的签名中选择最佳匹配-在处理泛型时,这通常意味着 SomeMethod<T>(T) 会被认为是比 SomeMethod<T>( IEnumerable<T> ) …尤其是当你的参数 T[] List<T> .

    但从根本上讲,您必须考虑操作单个值与一组值是否确实是 相同操作 . 如果它们在逻辑上是不同的,那么为了清晰起见,您可能需要使用不同的名称。也许有一些用例可以证明单个对象和对象集合之间的语义差异没有意义……但是在这种情况下,为什么要实现两种不同的方法呢?尚不清楚方法重载是表达差异的最佳方法。让我们来看一个导致混淆的例子:

    Cache.GetOrAdd("abc", () => context.Customers.Frobble() );
    

    首先,注意在上面的示例中,我们选择忽略返回参数。其次,注意我们调用了一些方法 Frobble() Customers 收集。现在你能告诉我哪个超负荷的 GetOrAdd() 会被呼叫吗?很明显,不知道 泡沫() 返回它是不可能的。 我个人认为,如果可能的话,应该避免使用那些语义不能从语法中轻易推断出来的代码。 如果我们选择更好的名字,这个问题就会得到缓解:

    Cache.Add( "abc", () => context.Customers.Frobble() );
    Cache.AddRange( "xyz", () => context.Customers.Frobble() );
    

    最后,只有三个选项可以消除示例中的方法的歧义:

    1. 更改其中一个方法的名称。
    2. 铸造成 IEnumerable<T> 无论你叫第二个超负荷。
    3. 以编译器可以区分的方式更改其中一个方法的签名。

    选项1是不言而喻的,所以我不再谈论它。

    选项2也很容易理解:

    var customers = Cache.GetOrAdd("All", 
         () => (IEnumerable<Customer>)context.Customers.ToArray());
    

    方案3更复杂。让我们来看看我们如何实现它。

    方法是通过更改 Func<> 委托,例如:

     T GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem)
    T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem)
    
    // now we can do:
    var customer = Cache.GetOrAdd("First", _ => context.Customers.First());
    var customers = Cache.GetOrAdd("All", () => context.Customers.ToArray());
    

    就我个人而言,我觉得这个选项非常难看、不讲理、令人困惑。引入一个未使用的参数是很糟糕的…但遗憾的是,它会奏效的。

    更改签名的另一种方法(不那么糟糕)是使返回值 out 参数:

    void GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem, out T);
    void GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem, out T[])
    
    // now we can write:
    Customer customer;
    Cache.GetOrAdd("First", _ => context.Customers.First(), out customer);
    
    Customer[] customers;
    var customers = Cache.GetOrAdd("All", 
                                   () => context.Customers.ToArray(), out customers);
    

    但这真的更好吗?它阻止我们将这些方法用作其他方法调用的参数。这也使得代码不那么清晰和不容易理解。

    我将介绍的最后一种替代方法是向标识返回值类型的方法添加另一个通用参数:

    T GetOrAdd<T> (string cachekey, Func<T> fnGetItem);
    R[] GetOrAdd<T,R> (string cachekey, Func<IEnumerable<T>> fnGetItem);
    
    // now we can do:
    var customer = Cache.GetOrAdd("First", _ => context.Customers.First());
    var customers = Cache.GetOrAdd<Customer,Customer>("All", () => context.Customers.ToArray());
    

    所以可以使用提示来帮助编译器为我们选择一个重载…当然。但是,看看作为开发人员,我们要完成的所有额外工作(更不用说引入的丑陋和出错的机会)。 这真的值得付出努力吗?特别是当一个简单可靠的技术(命名方法不同)已经存在帮助我们?

        4
  •  1
  •   Steven    15 年前

    约束不支持排除,这一点一开始可能看起来很令人沮丧,但却是一致的,而且很有意义(例如,考虑到接口并不规定什么实现) 不能 做)。

    也就是说,您可以利用IEnumerable重载的约束来进行操作……也许可以将方法更改为具有两个泛型类型 <X, T> 有一个约束,比如“ where X : IEnumerable<T> “?

    ETA以下代码示例:

      void T[] GetOrAdd<X,T> (string cachekey, Func<X> fnGetItem) 
                where X : IEnumerable<T>
        { 
        }