Дискриминационный союз в C # - PullRequest
80 голосов
/ 30 июня 2010

[Примечание: у этого вопроса было оригинальное название " Объединение стилей C (ish) в C # " но, как сообщил мне комментарий Джеффа, очевидно, что эта структура называется «дискриминационным союзом»]

Извините за многословность этого вопроса.

Есть несколько похожих вопросов на мой, уже в SO, но они, кажется, концентрируются на преимуществах объединения памяти или использования его для взаимодействия. Вот пример такого вопроса .

Мое желание иметь союзную вещь несколько отличается.

В данный момент я пишу некоторый код, который генерирует объекты, похожие на эти

public class ValueWrapper
{
    public DateTime ValueCreationDate;
    // ... other meta data about the value

    public object ValueA;
    public object ValueB;
}

Довольно сложные вещи, я думаю, вы согласитесь. Дело в том, что ValueA может быть только нескольких определенных типов (скажем, string, int и Foo (который является классом), а ValueB может быть другим небольшим набором типов. Мне не нравится воспринимать эти значения как объекты (я хочу теплое уютное чувство кодирования с некоторой безопасностью типов).

Поэтому я подумал о написании тривиального небольшого класса-обертки, чтобы выразить тот факт, что ValueA логически является ссылкой на определенный тип. Я позвонил в класс Union, потому что то, что я пытаюсь достичь, напомнило мне о концепции объединения в C.

public class Union<A, B, C>
{
    private readonly Type type; 
    public readonly A a;
    public readonly B b;
    public readonly C c;

    public A A{get {return a;}}
    public B B{get {return b;}}
    public C C{get {return c;}}

    public Union(A a)
    {
        type = typeof(A);
        this.a = a;
    }

    public Union(B b)
    {
        type = typeof(B);
        this.b = b;
    }

    public Union(C c)
    {
        type = typeof(C);
        this.c = c;
    }

    /// <summary>
    /// Returns true if the union contains a value of type T
    /// </summary>
    /// <remarks>The type of T must exactly match the type</remarks>
    public bool Is<T>()
    {
        return typeof(T) == type;
    }

    /// <summary>
    /// Returns the union value cast to the given type.
    /// </summary>
    /// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
    public T As<T>()
    {
        if(Is<A>())
        {
            return (T)(object)a;    // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types? 
            //return (T)x;          // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
        }

        if(Is<B>())
        {
            return (T)(object)b; 
        }

        if(Is<C>())
        {
            return (T)(object)c; 
        }

        return default(T);
    }
}

Использование этого класса ValueWrapper теперь выглядит так

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}

, что-то вроде того, чего я хотел достичь, но я упускаю один довольно важный элемент - это принудительная проверка типов при вызове функций Is и As, как показано в следующем коде

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }

IMO Недопустимо спрашивать ValueA, если это char, поскольку в его определении четко сказано, что это не так - это ошибка программирования, и я хотел бы, чтобы компилятор подобрал это. [Также, если бы я мог сделать это правильно, то (надеюсь), я бы также получил intellisense - что было бы благом.]

Чтобы добиться этого, я бы хотел сказать компилятору, что тип T может быть одним из A, B или C

    public bool Is<T>() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 

Кто-нибудь знает, возможно ли то, чего я хочу достичь? Или я просто глуп, когда пишу этот класс?

Заранее спасибо.

Ответы [ 15 ]

1 голос
/ 30 июня 2010

Вы можете генерировать исключения, когда есть попытка получить доступ к переменным, которые не были инициализированы, то есть, если они созданы с параметром A, а затем есть попытка получить доступ к B или C, это может вызвать, скажем, UnsupportedOperationException. Вам нужен геттер, чтобы заставить его работать.

0 голосов
/ 30 июля 2018

Группа разработчиков языка C # обсуждала дискриминационные союзы в январе 2017 года

Вы можете проголосовать за запрос функции на https://github.com/dotnet/csharplang/issues/113

0 голосов
/ 23 июля 2017

Я использую собственный Union Type.

Рассмотрим пример, чтобы прояснить ситуацию.

Представьте, что у нас есть класс контактов:

public class Contact 
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string PostalAdrress { get; set; }
}

Все они определены как простые строки, но на самом деле это просто строки? Конечно, нет. Имя может состоять из имени и фамилии. Или электронная почта - это просто набор символов? Я знаю, что по крайней мере он должен содержать @, и это обязательно.

Давайте улучшим нам модель домена

public class PersonalName 
{
    public PersonalName(string firstName, string lastName) { ... }
    public string Name() { return _fistName + " " _lastName; }
}

public class EmailAddress 
{
    public EmailAddress(string email) { ... } 
}

public class PostalAdrress 
{
    public PostalAdrress(string address, string city, int zip) { ... } 
}

В этих классах будут проверки при создании, и в итоге у нас будут действительные модели. Consturctor в классе PersonaName требует имен FirstName и LastName одновременно. Это означает, что после создания оно не может иметь недопустимое состояние.

И контактный класс соответственно

public class Contact 
{
    public PersonalName Name { get; set; }
    public EmailAdress EmailAddress { get; set; }
    public PostalAddress PostalAddress { get; set; }
}

В этом случае у нас та же проблема, объект класса Contact может быть в недопустимом состоянии. Я имею в виду, что у него может быть EmailAddress, но нет имени

var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };

Давайте исправим это и создадим класс Contact с конструктором, который требует PersonalName, EmailAddress и PostalAddress:

public class Contact 
{
    public Contact(
               PersonalName personalName, 
               EmailAddress emailAddress,
               PostalAddress postalAddress
           ) 
    { 
         ... 
    }
}

Но здесь у нас другая проблема. Что делать, если у Person есть только EmailAdress и нет PostalAddress?

Если мы подумаем об этом, мы поймем, что существует три возможности действительного состояния объекта класса Contact:

  1. Контакт имеет только адрес электронной почты
  2. Контакт имеет только почтовый адрес
  3. Контакт имеет адрес электронной почты и почтовый адрес

Давайте выпишем доменные модели. Для начала создадим класс Contact Info, состояние которого будет соответствовать вышеприведенным случаям.

public class ContactInfo 
{
    public ContactInfo(EmailAddress emailAddress) { ... }
    public ContactInfo(PostalAddress postalAddress) { ... }
    public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}

А Контактный класс:

public class Contact 
{
    public Contact(
              PersonalName personalName,
              ContactInfo contactInfo
           )
    {
        ...
    }
}

Давайте попробуем использовать это:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases

Давайте добавим метод Match в класс ContactInfo

public class ContactInfo 
{
   // constructor 
   public TResult Match<TResult>(
                      Func<EmailAddress,TResult> f1,
                      Func<PostalAddress,TResult> f2,
                      Func<Tuple<EmailAddress,PostalAddress>> f3
                  )
   {
        if (_emailAddress != null) 
        {
             return f1(_emailAddress);
        } 
        else if(_postalAddress != null)
        {
             ...
        } 
        ...
   }
}

В методе match мы можем написать этот код, потому что состояние класса контакта контролируется конструкторами и может иметь только одно из возможных состояний.

Давайте создадим вспомогательный класс, чтобы каждый раз не писать столько кода.

public abstract class Union<T1,T2,T3>
    where T1 : class
    where T2 : class
    where T3 : class
{
    private readonly T1 _t1;
    private readonly T2 _t2;
    private readonly T3 _t3;
    public Union(T1 t1) { _t1 = t1; }
    public Union(T2 t2) { _t2 = t2; }
    public Union(T3 t3) { _t3 = t3; }

    public TResult Match<TResult>(
            Func<T1, TResult> f1,
            Func<T2, TResult> f2,
            Func<T3, TResult> f3
        )
    {
        if (_t1 != null)
        {
            return f1(_t1);
        }
        else if (_t2 != null)
        {
            return f2(_t2);
        }
        else if (_t3 != null)
        {
            return f3(_t3);
        }
        throw new Exception("can't match");
    }
}

Мы можем заранее иметь такой класс для нескольких типов, как это делается с делегатами Func, Action. 4-6 параметров универсального типа будут полностью для класса Union.

Перепишем ContactInfo класс:

public sealed class ContactInfo : Union<
                                     EmailAddress,
                                     PostalAddress,
                                     Tuple<EmaiAddress,PostalAddress>
                                  >
{
    public Contact(EmailAddress emailAddress) : base(emailAddress) { }
    public Contact(PostalAddress postalAddress) : base(postalAddress) { }
    public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}

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

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console
    .WriteLine(
        contact
            .ContactInfo()
            .Match(
                (emailAddress) => emailAddress.Address,
                (postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
                (emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
            )
    );

Вот и все. Я надеюсь, вам понравилось.

Пример взят с сайта F # для удовольствия и прибыли

0 голосов
/ 01 июля 2010

Невозможно сделать именно тот синтаксис, который вы использовали, но с немного большей детализацией и копированием / вставкой, легко сделать разрешение перегрузки выполнением работы за вас:


// this code is ok
var u = new Union("");
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

// and this one will not compile
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

К настоящему моменту этодолжно быть довольно очевидно, как это реализовать:


    public class Union
    {
        private readonly Type type;
        public readonly A a;
        public readonly B b;
        public readonly C c;

        public Union(A a)
        {
            type = typeof(A);
            this.a = a;
        }

        public Union(B b)
        {
            type = typeof(B);
            this.b = b;
        }

        public Union(C c)
        {
            type = typeof(C);
            this.c = c;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(A) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(B) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(C) == type;
        }

        public A Value(GetValueTypeSelector _)
        {
            return a;
        }

        public B Value(GetValueTypeSelector _)
        {
            return b;
        }

        public C Value(GetValueTypeSelector _)
        {
            return c;
        }
    }

    public static class Is
    {
        public static TypeTestSelector OfType()
        {
            return null;
        }
    }

    public class TypeTestSelector
    {
    }

    public static class Get
    {
        public static GetValueTypeSelector ForType()
        {
            return null;
        }
    }

    public class GetValueTypeSelector
    {
    }

Нет проверок для извлечения значения неправильного типа, например:


var u = Union(10);
string s = u.Value(Get.ForType());

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

0 голосов
/ 30 июня 2010

Вы можете экспортировать функцию сопоставления псевдо-паттернов, как я использую для типа Either в моей библиотеке Sasa . В настоящее время существуют накладные расходы времени выполнения, но я в конечном итоге планирую добавить анализ CIL, чтобы встроить всех делегатов в истинный оператор case.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...