简化通用类型推断。

10

我正在编写一段通用代码,用于处理从多个来源加载数据的情况。我有一个方法,其签名如下:

public static TResult LoadFromAnySource<TContract, TSection, TResult>
    (this TSection section, 
          string serviceBaseUri, 
          string nodeName)
    where TSection : ConfigurationSection
    where TResult : IDatabaseConfigurable<TContract, TSection>, new() 
    where TContract : new()

但这有些过度:当我传递 TResult 时,我已经知道 TContractTSection 的确切含义。在我的示例中:

public interface ISourceObserverConfiguration 
    : IDatabaseConfigurable<SourceObserverContract, SourceObserverSection>

但是我必须写以下内容:
sourceObserverSection.LoadFromAnySource<SourceObserverContract, 
                                        SourceObserverSection, 
                                        SourceObserverConfiguration>
    (_registrationServiceConfiguration.ServiceBaseUri, nodeName);

您可以看到,我必须两次指定<SourceObserverContract, SourceObserverSection>这对键值对,这违反了DRY原则。因此,我想写出类似以下的内容:
sourceObserverSection.LoadFromAnySource<SourceObserverConfiguration>
   (_registrationServiceConfiguration.ServiceBaseUri, nodeName);

使SourceObserverContractSourceObserverSection从接口中推断出来。

C#中是否可能,还是我需要手动指定?

IDatabaseConfigurable看起来像:

public interface IDatabaseConfigurable<in TContract, in TSection> 
    where TContract : ConfigContract
    where TSection : ConfigurationSection
{
    string RemoteName { get; }

    void LoadFromContract(TContract contract);

    void LoadFromSection(TSection section);
}

这个扩展程序根据一些逻辑调用这两个方法。我必须指定类型,因为我需要访问每个具体实现的属性,所以我需要协变性。


1
如果您的方法签名是 IDatabaseConfigurable<TContract, TSection> LoadFromAnySource(this TSection section, string serviceBaseUri, string nodeName, Func<TContract> contractCreator)(或者只是 TContract contract),则类型可以从使用中推断出来。 - Jeroen Mostert
1
这怎么违反了DRY原则?它比“不要重复写相同的东西”更加复杂。你可能并不在意在twitter中三次写t,对吧? :D 这两个并相同——一个比另一个更加通用。无论如何,@JeroenMostert的评论几乎是最好的方法,并且应该完全成为一个答案。虽然可能更适合像程序员这样的地方,而不是SO——我真的不认为这是一个适合SO的好问题。 - Luaan
@JeroenMostert 是的,但他们不知道要使用哪个“IDatabaseConfigurable”。 - Rob
1
是的,抱歉,显然我指的是 Func<IDatabaseConfigurable<...>>。一个“有效”的结果是实现了该接口的结果--如果需要更多的东西,那么设计上就有问题了。(事实上,我相当确定设计上存在一些需要重构的问题,但这不是我们讨论的主题。) - Jeroen Mostert
1
“Wrong”太强烈了。我只是想说,你可能可以以某种方式构建你的代码,使类型推断隐式地工作(或者也许你应该使用更少的泛型)。这是否真的可行,以及你是否喜欢结果完全是另一回事。我无法有意义地回答这些问题,而不进一步深入你的代码,但那将会退化为一个设计讨论,这是主观的,明确地与Stack Overflow无关。对于你所问的字面意思(“我能用我拥有的东西避免做这件事吗”)的答案是否定的,简单明了。 - Jeroen Mostert
显示剩余5条评论
3个回答

2
不行,类型推断不考虑方法的返回类型。虽然 TResult 可能包含所有必要的信息,但类型推断不会使用它。
需要在方法签名中加入 TContract 才能使其类型被推断。 TResult 是多余的,没有必要使其成为泛型,只需将 IDataBaseConfigurable<TContract, TSection> 用作方法的返回类型即可。

这里讨论了可能的解决方案:https://github.com/dotnet/roslyn/issues/5023 - Alex Zhukovskiy

1

根据当前的LoadFromAnySource方法签名,您无法推断出您想要的结果。但是,通过修改LoadFromAnySource签名,可以推断出这一点。

由于您已经了解ISourceObserverConfiguration接口(从中我们知道它重新实现了IDatabaseConfigurable<SourceObserverContract,SourceObserverSection>接口),因此在方法声明中使用它作为通用约束:

而不是

public static TResult LoadFromAnySource<TContract, TSection, TResult>
    (this TSection section, 
          string serviceBaseUri, 
          string nodeName)
    where TSection : ConfigurationSection
    where TResult : IDatabaseConfigurable<TContract, TSection>, new() 
    where TContract : new()

使用此代码。
public static TResult LoadFromAnySource<TResult>
    (this SourceObserverSection section, 
          string serviceBaseUri, 
          string nodeName)
    where TResult : ISourceObserverConfiguration, new()

这样就不需要像在ISourceObserverConfiguration接口中所知道的那样使用TContractTSection。编译器知道接口约束是IDatabaseConfigurable<SourceObserverContract, SourceObserverSection>,它会自动工作。
另外,由于这是一个扩展方法,并且我们正在定义对ISourceObserverConfiguration的通用约束,因此我们需要扩展SourceObserverSection
然后您可以按照您想要的方式使用它:
sourceObserverSection.LoadFromAnySource<SourceObserverConfiguration>
   (_registrationServiceConfiguration.ServiceBaseUri, nodeName);

更新

根据提问者对问题的修改/澄清,我得出以下结论:

C#中是否可能实现自动推断或者我需要手动指定?

你需要手动指定。基于要求需要进行重新实现的接口定义了顶层约束(top-level constraint)尝试解决的具体类型,因此无法基于此进行推断。换句话说,由于你有多个IDatabaseConfigurable的实现,调用方必须通过其 TContractTSection 约束来指定使用哪个实现。


由于您不再定义“TSection”,因此无法编译。 - juharr
我更新了我的回答,如果现在是正确的话,我会感激您的点赞。 - David Pine
ISourceObserverConfiguration 是特定接口之一,我有多个接口都继承自 IDatabaseConfigurable。所以不幸的是我不能在这里写 ISourceObserverConfiguration,它应该更加通用。 - Alex Zhukovskiy

1

这有点取决于你的代码有多灵活,以及你对它做了什么。一般来说,不行 - 你要么需要指定所有通用类型,要么不指定。

这意味着简单地传递TResult并不意味着其他通用类型已解析(尽管从逻辑上讲,它们可以被解析)。

根据你能够改变定义的程度,你可以使其更加整洁:

public static class Helper
{
    public static TResult LoadFromAnySource<TResult>(this ConfigurationSection section, string serviceBaseUri, string nodeName)
        where TResult : IDatabaseConfigurable<object, ConfigurationSection>, new()
    {
        return default(TResult);
    }
}

public class ConfigurationSection { }
public interface IDatabaseConfigurable<out TContract, out TSection> 
    where TContract : new()
    where TSection : ConfigurationSection
{ 
}

public class DatabaseConfigurable<TContract, TSection> : IDatabaseConfigurable<TContract, TSection>
    where TContract : new()
    where TSection : ConfigurationSection
{ 
}

public class SourceObserverContract { }
public class SourceObserverSection : ConfigurationSection { } 

这让你可以写:

var sect = new ConfigurationSection();
sect.LoadFromAnySource<DatabaseConfigurable<SourceObserverContract, SourceObserverSection>>("a", "B");

区别在于您将限制放在IDatabaseConfigurable上,而不是方法上。您还需要使接口协变。如果您的设计不允许这样做,则据我所见,除非有一个非泛型的IDatabaseConfigurable,否则无法实现您要完成的任务。

我知道,但有时候可以通过在类级别提取一些类型来避免这种情况。例如,当我们可以推断出BC时,我们需要指定所有参数的情况下,可以使用Foo.Bar<A,B,C>(B b, C c),但是Foo<A>.Bar(B b, C c)可以避免这种情况。但是我不知道如何在这里实现相同(或类似)的技巧... - Alex Zhukovskiy
在这种情况下,不需要,因为所有类型都来自通用参数(除了ConfigurationSection,我已经移动 - 与您在此处的示例相同)。由于我们不传递类型,如果没有协变接口或非泛型接口,则不可能实现。 - Rob
我赞成它,但不幸的是我不能使用它,因为调用者应该得到一个确切类型的最终结果。当然它可以强制转换,但是当协变性为我们完成一切时,这样做更好。 - Alex Zhukovskiy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接