Перегрузка общих методов

При вызове общего метода для хранения объекта иногда приходится обрабатывать определенный тип по-разному. Я знаю, что вы не можете перегружать на основе ограничений, но любая другая альтернатива, кажется, представляет свои проблемы.

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 ... }

Однако, если вы не знаете, что эта функция существует, и вы пытаетесь использовать общий (Сохранить), тогда вы можете столкнуться с множеством проблем, которые должен был исправить "одноразовый". Это может быть еще хуже, если появляется новый разработчик, видит проблему с общим и решает, что он исправит ее своей собственной одноразовой функцией.

Итак...

Каковы варианты работы вокруг этой, казалось бы, общей проблемы?

Я просмотрел и использовал UnitOfWork и сейчас, это единственный вариант, который фактически решает проблему - но похоже, что атакует муху кувалдой.

Ответ 1

Вы можете сделать:

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);
    }
}

Ответ 2

Поскольку функции и перегрузки операторов, связанные с генериками, привязаны во время компиляции, а не во время выполнения, если код имеет два метода:

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

тогда код, который пытается вызвать Save(Foo), где Foo является переменной некоторого родового типа, всегда будет вызывать прежнюю перегрузку, даже если общий тип оказывается SomeClass. Мое предложение разрешить это было бы определить общий интерфейс ISaver<in T> с помощью не общего метода DoSave(T param). Пусть класс, который предоставляет метод Save, реализует все соответствующие общие интерфейсы для типов, которые он может обрабатывать. Затем попробуйте применить метод Save<T> объекта this к ISaver<T>. Если листинг преуспевает, используйте полученный ISaver<T>; в противном случае выполните общее сохранение. Если объявление типа класса содержит список всех соответствующих интерфейсов для типов, которые он может сохранить, этот подход будет отправлять вызовы Save на соответствующие методы.

Ответ 3

Ну, в основном, С# не разрешает специализацию шаблона, кроме как наследует такое наследование:

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))
    // ...

EDIT: для неизвестных типов довольно часто используется следующий шаблон:

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 FooRepository.Save<Foo>(Foo entity) instead of FooRepository.Save(Foo entity)!

Используя RTTI, существует только один метод Save, и все будет в порядке.

Ответ 4

Зачем использовать разные имена для вашего метода?

См. следующее:

    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();
        }
    }