泛型方法的重载

23

当调用通用方法存储对象时,偶尔需要以不同的方式处理特定类型。我知道你不能基于约束重载,但任何其他替代方案似乎都会带来自己的问题。

public bool Save<T>(T entity) where T : class
{ ... some storage logic ... }

我希望能做类似以下的事情:
public bool Save<SpecificClass>(T entity)
{ ... special logic ... }

过去,我们的团队曾经创建了以下的“一次性”方法来保存这些类:
public bool SaveSpecificClass(SpecificClass sc)
{ ... special logic ... }

然而,如果你不知道该功能的存在,并尝试使用通用的(Save)函数,则可能会遇到一系列问题,这些问题本应由“一次性”修复。如果一个新的开发者来了,看到了通用的问题,并决定用他自己的“一次性”函数来解决它,情况会更糟。那么,有哪些解决这个看似常见问题的方法呢?我已经查看并使用了UnitOfWork,目前似乎这是唯一真正解决问题的选项 - 但似乎有点大材小用。

3
与C ++不同,C#不允许模板专门化。 - Panagiotis Kanavos
这些Save()方法是放在一些轻量级的帮助类中还是实体类中?我在考虑继承,但重要的是要确定这是正确的方式,因为继承并不总是被正确使用。 - sll
可能是重复问题 如何在C#中进行模板特化 - Panagiotis Kanavos
4个回答

23
你可以做以下的事情:
public bool Save<T>(T entity) where T : class
{ ... some storage logic ... }

public bool Save(SpecificClass entity)
{ ... special logic ... }

例如:

public class SpecificClass
{
}

public class Specializer
{
    public bool GenericCalled;
    public bool SpecializedCalled;

    public bool Save<T>(T entity) where T : class
    {
        GenericCalled = true;
        return true;
    }

    public bool Save(SpecificClass entity)
    {
        SpecializedCalled = true;
        return true;
    }
}

public class Tests
{
    [Test]
    public void TestSpecialization()
    {
        var x = new Specializer();
        x.Save(new SpecificClass());
        Assert.IsTrue(x.SpecializedCalled);
        Assert.IsFalse(x.GenericCalled);
    }
}

3
无论如何我都无法让它起作用:通用版本的函数总是被调用。 - MarkusParker
@MarkusParker,你能提供一个例子吗?因为我的更新显示它在单元测试中运行良好。请记住,用于传递SpecificClass的变量必须是SpecificClass类型,除非它是一个dynamic类,因为C#支持编译时多态性。 - Peter Ritchie
类测试 { public bool GenericCalled; public bool SpecializedCalled; public bool Save(T entity) { GenericCalled = true; return true; } public bool Save(int entity) { SpecializedCalled = true; return true; } public void HandleGenericType() { Save(default(T)); } }在这里,HandleGenericType<int>() 调用了泛型版本。 - MarkusParker
4
啊,好的。那是编译时多态性在发挥作用。在编译时,HandleGenericType 不知道一个 int 可以被调用并特别调用 Save(int),因此它链接到通用的 Save<T>。你可以通过将 default<T> 强制转换为 dynamic 来实现运行时多态性。例如:Save((dynamic)default(T)); - Peter Ritchie
@PeterRitchie - 非常感谢!!我已经谷歌了20分钟,试图找到答案,而这就是它! - Iamsodarncool
显示剩余4条评论

7

基本上,除了通过继承这种方式之外,C# 并不允许模板特化,如下所示:

interface IFoo<T> { }
class Bar { }

class FooBar : IFoo<Bar> { }

至少在编译时不支持此功能。但是,您可以使用RTTI来实现您想要的功能:

public bool Save<T>(T entity)
{
    // Check if "entity" is of type "SpecificClass"
    if (entity is SpecificClass)
    {
        // Entity can be safely casted to "SpecificClass"
        return SaveSpecificClass((SpecificClass)entity);
    }

    // ... other cases ...
}

is表达式非常方便进行运行时类型检查。它的工作方式类似于以下代码:

if (entity.GetType() == typeof(SpecificClass))
    // ...

编辑:对于未知类型来说,使用以下模式是非常常见的:

if (entity is Foo)
    return DoSomethingWithFoo((Foo)entity);
else if (entity is Bar)
    return DoSomethingWithBar((Bar)entity);
else
    throw new NotSupportedException(
        String.Format("\"{0}\" is not a supported type for this method.", entity.GetType()));
编辑2: 正如其他答案建议的那样,使用SpecializedClass重载方法时,如果您正在使用多态性,则需要注意。如果您正在为存储库使用接口(实际上这是设计存储库模式的好方法),则存在一些情况,重载可能导致调用错误的方法,无论您是否将SpecializedClass对象传递给接口。
interface IRepository
{
    bool Save<T>(T entity)
        where T : class;
}

class FooRepository : IRepository
{
    bool Save<T>(T entity)
    {
    }

    bool Save(Foo entity)
    {
    }
}

如果您使用FooRepository.Save直接调用Foo的实例,则此方法有效:

var repository = new FooRepository();
repository.Save(new Foo());

但是,如果您正在调用接口(例如,如果您正在使用模式来实现存储库创建),则此方法不起作用:
IRepository repository = GetRepository<FooRepository>();
repository.Save(new Foo());  // Attention! Call's FooRepository.Save<Foo>(Foo entity) instead of FooRepository.Save(Foo entity)!

使用运行时类型识别(RTTI),只需一个“Save”方法,您就可以顺利完成。

1
这句话没有错,但它仍然是一个重载,而不是一个特化。 ;) - Carsten
没有人说过不同的事情...我只是在回答OP的评论“然而,如果你不知道函数存在,而你尝试使用通用的(Save),那么你可能会遇到一系列问题,这个‘一次性’函数本来就应该解决。如果一个新的开发者出现了,看到了通用的问题,并决定用自己的‘一次性’函数来解决它,这会让情况变得更糟。”。而且它也有效。这只是已经提供的答案的另一种选择。没有必要对它进行负面评价... - Carsten
我更喜欢使用RTTI,因为它使开发者更容易做对(只需要调用一种方法),而不是做错(可能调用错误的方法)。还要看一下问题的注释,因为专业化是提问者所做的(无论他是否要求"重载")。 - Carsten
事实上,我的回答也没有提供“模板特化”,但行为也是相同的!那么为什么要给它点踩呢? - Carsten
顺便说一下... "模板特化"和你描述的"重载"是有区别的:想象一下接口IFoo定义了一个方法Save<T>。特化CFoo实现了IFoo并提供了一个像你描述的Save(Specialized s)的重载。客户端调用接口IFoo foo = GetFoo(); foo.Save(new Specialized());哪种行为更像OP想要的?RTTI还是重载?!对的... RTTI可以工作,而重载仍然会调用Save<T>。;-) - Carsten
显示剩余5条评论

5

由于涉及泛型的函数和操作符重载是在编译时绑定而不是运行时绑定,因此如果代码有两个方法:

public bool Save<T>(T entity) ...
public bool Save(SomeClass entity) ...

如果尝试调用 Save(Foo),其中 Foo 是某种通用类型的变量,则代码将始终调用前面的重载,即使通用类型恰好是 SomeClass。我的建议是定义一个带有非泛型方法 DoSave(T param) 的通用接口 ISaver<in T>。提供 Save 方法的类应实现其可以处理的所有适当的通用接口。然后,对象的 Save<T> 方法尝试将 this 转换为 ISaver<T>。如果转换成功,则使用生成的 ISaver<T>;否则执行通用保存。只要类类型声明列出了它可以保存的所有适当接口,这种方法就会将 Save 调用分派到正确的方法。


0

为什么要在方法中使用不同的名称?

请看以下内容:

    public class Entity
    {
    }

    public class SpecificEntity : Entity
    {
    }

    public class Program
    {
        public static void Save<T>(T entity)
            where T : class
        {
            Console.WriteLine(entity.GetType().FullName);
        }

        public static void Save(SpecificEntity entity)
        {
            Console.WriteLine(entity.GetType().FullName);
        }

        private static void Main(string[] args)
        {
            Save(new Entity());          // ConsoleApplication13.Entity
            Save(new SpecificEntity());  // ConsoleApplication13.SpecificEntity

            Console.ReadKey();
        }
    }

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