【问题标题】:C# method generic params parameter bug?C#方法泛型参数错误?
【发布时间】:2018-06-11 19:50:56
【问题描述】:

在我看来,C# 编译器中存在错误/不一致。

这很好用(第一个方法被调用):

public void SomeMethod(string message, object data);
public void SomeMethod(string message, params object[] data);

// ....
SomeMethod("woohoo", item);

但这会导致“以下方法之间的调用不明确”错误:

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);

// ....
SomeMethod("woohoo", (T)item);

我可以完全使用转储第一种方法,但由于这是一个对性能非常敏感的库,并且大约 75% 的时间将使用第一种方法,我宁愿不总是将东西包装在一个数组中并实例化一个如果只有一个项目,迭代器会遍历一个 foreach。

拆分成不同的命名方法充其量是 IMO 的混乱。

想法?

编辑:

我猜 Andrew 可能正在做点什么。

完整示例:

public static class StringStuffDoer
{
    public static string ToString<T>(T item1, T item2)
    {
        return item2.ToString() + item1.ToString();
    }

    public static string ToString<T>(T item, params T[] items)
    {
        StringBuilder builder = new StringBuilder();

        foreach (T currentItem in items)
        {
            builder.Append(currentItem.ToString());
        }

        return item.ToString() + builder.ToString();
    }

    public static void CallToString()
    {
        ToString("someString", null); // FAIL
        ToString("someString", "another string"); // SUCCESS
        ToString("someString", (string)null); // SUCCESS
    }
}

我仍然认为需要演员阵容很奇怪 - 这个电话并不模棱两可。如果将 T 替换为字符串或对象或任何非泛型类型,它会起作用,那么为什么它不适用于泛型呢?它正确地找到了两种可能的匹配方法,所以我相信按照规范,它应该尽可能选择不使用参数的方法。如果我在这里错了,请纠正我。

(不是这样)最终更新:

很抱歉带你们参加这个 tyraid,我显然已经盯着这个太久了......一晚上看泛型和参数太多了。非通用版本也会引发模棱两可的错误,我只是在我的模型测试中关闭了方法签名。

真正的最终更新:

好的,这就是我的非通用测试中没有出现问题的原因。我使用“对象”作为类型参数。 SomeMethod(object) 和 SomeMethod(params object[]) 不会抛出模棱两可的错误,我猜“null”会自动转换为“object”。我会说有点奇怪,但也许可以理解。

所以,奇怪的是,这个调用确实有效:

SomeMethod<object>("someMessage", null);

【问题讨论】:

  • 请发布一个简短但完整的程序来演示该问题,并告诉我们您使用的是哪个版本的 C#。
  • 如果你不将item转换成T,它是如何工作的?如果在您将项目声明为 T 类型之前的某个时间点,它是否按预期工作?
  • 请注意,“错误”是实际行为与指定行为之间的差异。实际行为与预期行为之间的差异将是“奇怪”或“意外结果”。您是否检查了相应的规范?
  • 我只是把演员放在那里,向您展示第二个示例中的项目是类型 T。演员阵容没有任何区别。
  • @Mike M:如果你不写实际的调用而不是SomeMethod("woohoo", (T)item);(至少Titem是什么),我们不可能帮助你。跨度>

标签: c# generics parameters


【解决方案1】:

在我看来,C# 编译器中似乎存在错误/不一致。

编译器中肯定存在错误和不一致。 您还没有找到其中之一。在所有这些情况下,编译器的行为完全正确并符合规范。

我正在尽我所能理解这个非常令人困惑的问题。让我试着把它分解成一系列问题。

为什么会成功并调用第一个方法?

public void SomeMethod(string message, object data);
public void SomeMethod(string message, params object[] data);
// ....
SomeMethod("woohoo", item);

(假设:该项目是除 object[] 之外的编译时类型的表达式。)

重载解决方案必须在两种适用的方法之间进行选择。第二种方法仅适用于其扩展形式。仅适用于其扩展形式的方法自动比适用于其正常形式的方法差。因此选择剩下的更好的方法。

为什么这会失败并出现歧义错误?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod("woohoo", (T)item);

不可能说,因为你不说“T”是什么。在这个例子中,T 来自哪里?声明了两个名为 T 的类型参数;这段代码是在其中一种方法的上下文中吗?由于这些都是 不同的 类型,都命名为 T ,因此可能会有所不同。或者这又是第三种称为 T 的类型?

由于该问题没有足够的信息来回答它,我将代表您提出一个更好的问题。

为什么这会失败并出现歧义错误?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod("woohoo", "hello");

没有。它成功了。 类型推断在两种方法中都为 T 选择了“字符串”。两种通用方法都适用;第二个适用于它的扩展形式,所以它输了。

好的,那为什么会失败并出现歧义错误?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod("woohoo", null);

没有。它因“无法推断 T”错误而失败。此处没有足够的信息来确定两种情况下的 T 是什么。由于类型推断未能找到候选方法,因此候选集为空,重载决策无从选择。

所以这成功了,因为...?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod("woohoo", (string)null);

类型推断推断这两种方法在使用“字符串”构造时都是候选方法。同样,第二种方法更糟糕,因为它仅适用于扩展形式。

如果我们不考虑类型推断会怎样?为什么会这样模棱两可?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod<string>("woohoo", null);

我们现在有三个适用的候选人。第一种方法,正常形式的第二种方法,以及扩展形式的第二种方法。展开后的表格被丢弃,因为展开后的效果比正常情况差。这留下了两种正常形式的方法,一种采用字符串,另一种采用字符串[]。哪个更好?

当面临这种选择时,我们总是选择具有更具体类型的那个。如果你说

public void M(string s) { ... }
public void M(object s) { ... }
...
M(null);

我们会选择字符串版本,因为字符串比对象更具体。每个字符串都是一个对象,但不是每个对象都是一个字符串。

字符串不能转换为字符串[]。 string[] 不能转换为字符串。两者都不比另一个更具体。因此,这是一个模棱两可的错误;有两个“最佳”候选人。

那为什么会成功,又是做什么的呢?

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, params T[] data);
// ...
SomeMethod<object>("woohoo", null);

再次,我们有三个候选人,而不是两个。我们像以前一样自动消除扩展形式,留下两个。在剩下的两种正常形式的方法中,哪个更好?

我们必须确定哪个更具体。每个对象数组都是一个对象,但不是每个对象都是一个对象数组。 object[] 比 object 更具体,所以我们选择调用带有 object[] 的版本。我们传递了一个空参数数组,这几乎肯定不是您想要的。

这就是为什么像你这样进行重载是一种非常糟糕的编程实践。当你做这种事情时,你会为你的用户引入各种疯狂的模棱两可的可能性。 请不要设计这样的方法。

设计这种逻辑的更好方法是:(请注意,我还没有真正编译过这段代码,这只是我的想法。)

static string ToString<T>(T t)
{
    return t == null ? "" : t.ToString();
}
static string ToString<T>(T t1, T t2)
{
    return ToString<T>(t1) + ToString<T>(t2);
}
static string ToString<T>(T t1, T t2, params T[] rest)
{
    string firstTwo = ToString<T>(t1, t2);
    if (rest == null) return firstTwo;
    var sb = new StringBuilder();
    sb.Append(firstTwo);
    foreach(T t in rest)
      sb.Append(ToString<T>(t));
    return sb.ToString();
}

现在每个案例都以合理的语义和良好的效率处理。对于任何给定的调用站点,您都可以立即准确地预测将调用哪个方法;只有三种可能性:一个论点,两个论点或两个以上的论点。每个都由特定的方法明确处理。

【讨论】:

  • 感谢您提供非常完整的答案,但是我是根据从 .NET Framework 本身获得的模式进行设计的 - string.Format 重载。 string.Format(string, object) 和 string.Format(string, params object[]) 在称为 string.Format("format", null) 时在技术上是不明确的,但它需要第一个。我现在明白为什么了,但我的问题的根源是错误地认为行为也适用于对象以外的其他类。它适用于 object 的唯一原因是 object[] 可以隐式转换为 object,这显然不适用于其他类型。
  • @Mike:确实,我希望 Format 不是这样设计的;我觉得很混乱。然而,这只是总体规划中的一个相当小的缺陷。
【解决方案2】:

似乎对我有用,您的其余代码是否如下所示?

class TestThing<T>
{
    public void SomeMethod(string message, T data)
    {
        Console.WriteLine("first");
    }
    public void SomeMethod(string message, params T[] data)
    {
        Console.WriteLine("second");
    }
}


class Program
{
    static void Main(string[] args)
    {
        var item = new object();
        var test_thing = new TestThing<object>();
        test_thing.SomeMethod("woohoo", item);
        test_thing.SomeMethod("woohoo", item, item);

        Console.ReadLine();
    }
}

在 .NET 3.5 上编译良好,运行时输出“first”,然后输出“second”。您正在使用/定位哪个版本?

编辑

上面代码的问题是编译器无法分辨null 是什么类型。假设它是一个字符串或一个字符串数组同样有效,因此为什么当它只是 null 时它是模棱两可的,而当你专门转换它时它不是模棱两可的(即你告诉编译器应该如何处理它)。

更新

“同样可以论证 SomeMethod(string, string) 和 SomeMethod (string, params string[]), 但它有效”

实际上,没有。你会遇到同样的模棱两可的方法问题。

public static string TypedToString(string item1, string item2)
{
  return "";
}

public static string TypedToString(string item1, params string[] items)
{
  return "";
}

public static void CallToString()
{
  TypedToString("someString", null); // FAIL
}

【讨论】:

  • 抱歉,更新了我的帖子以反映问题。当 params 值为 null 而没有强制转换时,行为不一致。
  • SomeMethod(string, string) 和 SomeMethod(string, params string[]) 也可以这样说,但它确实有效。
  • 它正在正确推断类型,因为它正在选择两种匹配方法并说“这些都匹配”。问题是在通用案例与非参数方法中未能选择非参数方法的不一致。只是在非泛型情况下选择非参数方法。
  • 抱歉 - 在您的示例中没有发现第一个参数也是 T 类型,因此它将从第一个参数推断字符串。不过,null 仍然是个问题,请参阅上面的更新。
  • 对不起,更新了我的帖子并标记为答案...显然我已经太久了...
【解决方案3】:

您应该将签名更改为更具体。例如:

void Foo(object o1);
void Foo(object o1, object o2);
void Foo(object o1, object o2, object o3, params object[] rest);

注意最后两个之间的区别。这解决了歧义问题(也适用于泛型)。

更新:

public void SomeMethod<T>(string message, T data);
public void SomeMethod<T>(string message, T data1, T data2, params T[] data);

只有两种方法。没有歧义。

【讨论】:

  • 我不应该写额外的方法。 SomeMethod(string, string) 和 SomeMethod (string, params string[]) 自己搞定就好了。
  • 您不需要额外的Foo 方法,这只是一个示例。您仍然有一个模棱两可的方法调用,它实际调用的是实现定义的,我不会讨价还价。
  • 我不确定您在说什么 - 行为已明确定义 - 如果可能,请选择匹配的非参数方法。它在非泛型中执行此操作,即使我说 SomeMethod("string", null),即使这在技术上是一个模棱两可的调用。不一致是这里唯一的问题。
  • 我猜你的更新是一种解决方法,但从实现的角度来看它仍然相当糟糕,我想我只是不喜欢不一致的行为。
【解决方案4】:

我想为遇到此问题的任何人添加此花絮,以解释让我感到困惑的 SomeMethod(object) 和 SomeMethod(params object[]) 的歧义问题。这是 Figo Fei 在 MSDN 论坛上为我挖掘的 C# 规范:

10.6.1.4 参数数组

使用 params 修饰符声明的参数是参数数组。

在执行重载决议时,具有参数数组的方法可以以其正常形式或扩展形式(第 7.4.3.1 节)适用。方法的扩展形式仅在方法的正常形式不适用并且仅当与扩展形式具有相同签名的方法尚未在同一类型中声明时才可用。

当参数数组的类型是 object[] 时,在方法的正常形式和单个对象参数的扩展形式之间会出现潜在的歧义。歧义的原因是 object[] 本身可以隐式转换为 object 类型。但是,这种歧义没有问题,因为如果需要,可以通过插入强制转换来解决。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多