Типизированные безопасные распознаваемые объединения в C # или: Как ограничить количество реализаций интерфейса? - PullRequest
7 голосов
/ 12 марта 2011

Во-первых, извините за длинный пост.По сути, мой вопрос таков:

Я пытаюсь воспроизвести в C # следующий тип объединения F #:

type Relation =
     | LessThan of obj * obj
     | EqualTo of obj * obj
     | GreaterThan of obj * obj

Кто-нибудь может предложить более простое решение на основе интерфейса, чем следующее?


interface IRelation // concrete types represent ◊ in the expression "Subject ◊ Object"
{
    object Subject { get; }
    object Object  { get; }
}

struct LessThanRelation    : IRelation { … }
struct EqualToRelation     : IRelation { … }
struct GreaterThanRelation : IRelation { … }

Все мои алгоритмы распознают эти три типа отношений, и только эти, поэтому мне нужно предотвратить любые дальнейшие реализации IRelation третьими лицами (то есть другими сборками).

Сноска: Для некоторых может случиться так, что, если я только правильно настрою свой интерфейс и алгоритмы с точки зрения ориентации / полиморфизма объектов, не должно иметь значения, чтоРеализация сторонних разработчиков вводится в методы моего алгоритма, если интерфейс реализован правильно.Это действительная критика.Но давайте просто предположим, что на данный момент я предпочитаю более функциональный стиль программирования, чем строгую объектную ориентацию в этом случае.

До сих пор моя лучшая идеяобъявите все вышеперечисленные типы как internal (т.е. они никогда не будут видны непосредственно посторонним лицам) и создайте тип прокси Relation, который будет единственным видимым типом для третьих сторон:

public struct Relation  // constructors etc. are omitted here for brevity's sake
{
    public RelationType Type { get { … /* concrete type of value -> enum value */ } }

    public Relation Subject  { get { return value.Subject; } }
    public Relation Object   { get { return value.Object;  } }

    internal readonly IRelation value;
}

public enum RelationType
{
    LessThan,
    EqualTo,
    GreaterThan
}

Всепока все хорошо, но это становится более сложным…

  • … если я выставлю фабричные методы для конкретных типов отношений:

    public Relation CreateLessThanRelation(…)
    {
        return new Relation { value = new LessThanRelation { … } };
    }
    
  • … Всякий раз, когда я выставляю алгоритм, работающий с типами отношений, потому что я должен отобразить из / в тип прокси:

    public … ExposedAlgorithm(this IEnumerable<Relation> relations)
    {
        // forward unwrapped IRelation objects to an internal algorithm method:
        return InternalAlgorithm(from relation in relations select relation.value);
    }
    

Ответы [ 3 ]

13 голосов
/ 12 марта 2011

Ограничение реализации интерфейса означает, что он на самом деле не действует как интерфейс (который должен принимать любую реализацию (замену), такую ​​как декораторы) - поэтому я не могу рекомендовать это.

Кроме того, обратите внимание, что за небольшим исключением обобщений обработка структуры как интерфейса приводит к упаковке.

Так что остается один интересный случай; абстрактный класс с конструктором private и известным числом реализаций как вложенных типов , что означает что у них есть доступ к приватному конструктору.

Теперь вы управляете подтипами, бокс не является проблемой (так как это класс), и меньше ожидания замены.

6 голосов
/ 13 марта 2011

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

public abstract class Relation
{
    internal Relation(object subject, object obj)
    {
        Subject = subject;
        Object = obj;
    }
    public object Subject { get; private set; }
    public object Object { get; private set; }
}

public sealed class LessThanRelation : Relation
{
    public LessThanRelation(object subject, object obj) : base(subject, obj) { }
}

public sealed class EqualToRelation : Relation
{
    public EqualToRelation(object subject, object obj) : base(subject, obj) { }
}

public sealed class GreaterThanRelation : Relation
{
    public GreaterThanRelation(object subject, object obj) : base(subject, obj) { }
}

Внешние сборки могут видеть все члены класса Relation за исключением внутреннего конструктора, который извне создает впечатление, что класс не имеет конструкторов, поэтому сторонние сборки не могут определять свои собственные реализации.

3 голосов
/ 12 марта 2011

Я бы пошел с идеей, основанной на enum.На самом деле, я бы тоже использовал это решение в F #.Поскольку у вас всегда есть только два аргумента, вам на самом деле не нужен дискриминированный союз:

// Note: with numbers assigned to cases, this becomes enum
type RelationType =      
  | LessThan = 1
  | EqualTo = 2
  | GreaterThan = 3

// Single-case union (could be record, depending on your style)
type Relation = 
  | BinaryRelation of RelationType * obj * obj

В общем, если вы хотите закодировать дискриминированный союз в C #, тогда, вероятно, лучше всего использовать абстрактный базовый класс , а затем наследуемый класс для каждого случая (с дополнительными полями).Поскольку вы не планируете расширять его, добавляя новые подклассы, вы можете определить тег enum , который перечисляет все возможные подтипы (так что вы можете легко реализовать «сопоставление с шаблоном» с помощью switch для тега).

...