代码之家  ›  专栏  ›  技术社区  ›  Vladi Pavelka

C泛型方法类型参数不是根据用法推断的

  •  24
  • Vladi Pavelka  · 技术社区  · 8 年前

    最近,我尝试了一种访问者模式的实现,在该模式中,我尝试使用通用接口强制实施accept&visit方法:

    public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable>
    {
        TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor);
    }
    

    -其目的是1)将特定类型的“foo”标记为可被此类访问者访问,而此类访问者又是“此类foo”类型的访问者;2)在实现的可访问类型上强制执行正确签名的接受方法,例如:

    public class Foo : IVisitable<Foo>
    {
        public TResult Accept<TResult>(IVisitor<TResult, Foo> visitor) => visitor.Visit(this);
    }
    

    到目前为止,访问者界面:

    public interface IVisitor<out TResult, in TVisitable> where TVisitable : IVisitable<TVisitable>
    {
        TResult Visit(TVisitable visitable);
    }
    

    -应1)将访问者标记为“能够访问”tvisitable 2)此tvisitable的结果类型(treult)应为什么3)根据每个tvisitable强制使用正确签名的访问方法访问者实现“能够访问”,如:

    public class CountVisitor : IVisitor<int, Foo>
    {
        public int Visit(Foo visitable) => 42;
    }
    
    public class NameVisitor : IVisitor<string, Foo>
    {
        public string Visit(Foo visitable) => "Chewie";
    }
    

    非常愉快,这让我写出:

    var theFoo = new Foo();
    int count = theFoo.Accept(new CountVisitor());
    string name = theFoo.Accept(new NameVisitor());
    

    很好。

    现在悲伤的时刻开始了,当我添加另一种可见类型时,比如:

    public class Bar : IVisitable<Bar>
    {
        public TResult Accept<TResult>(IVisitor<TResult, Bar> visitor) => visitor.Visit(this);
    }
    

    我们可以看到的就是 CountVisitor :

    public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar>
    {
        public int Visit(Foo visitable) => 42;
        public int Visit(Bar visitable) => 7;
    }
    

    它突然打破了接受方法中的类型推断!(这破坏了整个设计)

    var theFoo = new Foo();
    int count = theFoo.Accept(new CountVisitor());
    

    给我:

    “方法的类型参数 'Foo.Accept<TResult>(IVisitor<TResult, Foo>)' 无法从用法推断。“

    有人能解释一下为什么吗?只有一个版本的 IVisitor<T, Foo> 接口 伯爵访客 工具-或者,如果 IVisitor<T, Bar> 由于某种原因无法消除,两者都有相同之处 T - int ,=任何其他类型都不能在那里工作。当有不止一个合适的候选者时,类型推断会立即放弃吗?(有趣的事实:Resharper认为 int 在里面 theFoo.Accept<int>(...) 是多余的:p,即使没有它也无法编译)

    3 回复  |  直到 8 年前
        1
  •  9
  •   DPenner1    8 年前

    当有不止一个合适的候选者时,类型推断会立即放弃吗?

    是的,在这种情况下是这样的。尝试推断方法的泛型类型参数时( TResult )类型推理算法似乎失败 CountVisitor 对该类型有两个推论 IVisitor<TResult, TVisitable> .


    来自 C# 5 specification (我最近发现的),§7.5.2:

    Tr M<X1…Xn>(T1 x1 … Tm xm)

    使用窗体的方法调用 M(E1 …Em) 类型推断的任务是找到唯一的类型参数 S1…Sn 对于每个类型参数 X1…Xn 所以电话 M<S1…Sn>(E1…Em) 生效。

    编译器的第一步如下(§7.5.2.1):

    对于每个方法参数 Ei :

    • 如果 工程安装 是一个匿名函数, 显式参数类型推理 (§7.5.2.7)由以下材料制成: 工程安装 Ti

    • 否则,如果 工程安装 有一个类型 U xi 是一个值参数,然后是 下界推理 是制造的 U .

    你只有一个论点,所以我们只有 工程安装 是表达式 new CountVisitor() . 这显然不是匿名函数,所以我们在第二个要点。在我们的案例中,这是微不足道的, U 属于类型 伯爵访客 . “ 是一个值参数“位基本上意味着它不是一个 out , in , ref 等等,变量,这里就是这种情况。

    现在,我们需要 下界推理 伯爵访客 ivisitor<tresult,tvisitable> §7.5.2.9的相关部分(如果由于可变开关,我们 V = ivisitor<tresult,tvisitable> 在我们的案例中):

    • 否则,设置 U1…Uk V1…Vk 通过检查以下情况是否适用来确定:
      • V 是数组类型 V1[…] U 是数组类型 U1[…] (或有效基类型为 U1[_] )同级别的
      • V IEnumerable<V1> , ICollection<V1> IList<V1> U 是一维数组类型 U1[] (或有效基类型为 U1] )
      • V 是构造的类、结构、接口或委托类型 C<V1…Vk> 有一个独特的类型 C<U1…Uk> 这样的话 U (或者,如果 U 是类型参数,其有效基类或其有效接口集的任何成员)与(直接或间接)相同、继承自(直接或间接)或实现(直接或间接) C<U1UK> .

    (唯一性限制意味着在case接口中 C<T>{} class U: C<X>, C<Y>{} ,则当从 U C<T> 因为 U1 可以是 X Y )

    我们可以跳过前两种情况,因为它们显然不适用,第三种情况是我们所陷入的情况。编译器试图查找 独特的 类型 C<U1UK> 那个 伯爵访客 实施和发现 这种类型, IVisitor<int, Foo> IVisitor<int, Bar> . 请注意,规范给出的示例与您的示例几乎相同。

    由于唯一性约束,没有为此方法参数进行推理。由于编译器无法从参数中推断出任何类型信息,因此它没有继续尝试推断的内容。 侵权 因此失败了。


    至于为什么存在唯一性约束,我猜想它简化了算法,从而简化了编译器的实现。如果你感兴趣, here's a link 到Roslyn(现代C编译器)实现泛型方法类型推断的源代码。

        2
  •  13
  •   memo    8 年前

    似乎类型推断以贪婪的方式工作,首先尝试匹配 方法 泛型类型,然后是类泛型类型。所以如果你说

    int count = theFoo.Accept<int>(new CountVisitor());
    

    这很奇怪,因为foo是类泛型类型的唯一候选。

    首先,如果将方法泛型类型替换为第二类泛型类型,它将工作:

    public interface IVisitable<R, out T> where T: IVisitable<int, T>
    {
        R Accept(IVisitor<R, T> visitor);
    }
    
    public class Foo : IVisitable<int, Foo>
    {
        public int Accept(IVisitor<int, Foo> visitor) => visitor.Visit(this);
    }
    
    public class Bar : IVisitable<int, Bar>
    {
        public int Accept(IVisitor<int, Bar> visitor) => visitor.Visit(this);
    }
    
    public interface IVisitor<out TResult, in T> where T: IVisitable<int, T>
    {
        TResult Visit(T visitable);
    }
    
    public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar>
    {
        public int Visit(Foo visitable) => 42;
        public int Visit(Bar visitable) => 7;
    }
    
    class Program {
        static void Main(string[] args) {
            var theFoo = new Foo();
            int count = theFoo.Accept(new CountVisitor());
        }
    }
    

    其次(这是一个奇怪的部分,它突出了类型推断的工作原理),如果您替换 int 具有 string Bar 参观者:

    public class CountVisitor : IVisitor<int, Foo> , IVisitor<string, Bar>
    {
        public int Visit(Foo visitable) => 42;
        public string Visit(Bar visitable) => "42";
    }
    

    首先,您会得到相同的错误,但请注意如果强制使用字符串会发生什么:

        int count = theFoo.Accept<string>(new CountVisitor());
    

    错误CS1503:参数1:无法从转换 'CountVisitor' 'IVisitor<string, Foo>'

    这意味着编译器首先查看 方法 泛型类型( TResult 在你的情况下),如果它找到更多的候选人,就会立即失败。它甚至没有进一步研究类的泛型类型。

    我试图从Microsoft中找到一个类型推断规范,但找不到任何。

        3
  •  5
  •   Frederik Gheysels    8 年前

    在C中,您可以通过使用 dynamic 关键字。

    您可以这样实现访问者:

    public class CountVisitor : IVisitor<int, IVisitable>
    {
       public int Visit( IVisitable v )
       {
           dynamic d = v;
           Visit(d);
       }
    
        private int Visit( Foo f ) 
        {
            return 42;
        }
    
        private int Visit( Bar b )
        {
            return 7;
        }
    }
    

    通过这样做,您将不需要在上实现accept方法 Foo Bar 尽管它们仍然必须为 Visitor 不按计划工作。