Загальне обмеження типу множинних (АБО) типів


135

Читаючи це , я дізнався, що можна дозволити методу приймати параметри декількох типів, зробивши його загальним методом. У прикладі наступний код використовується з обмеженням типу, щоб переконатися, що "U" є an IEnumerable<T>.

public T DoSomething<U, T>(U arg) where U : IEnumerable<T>
{
    return arg.First();
}

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

public void test<T>(string a, T arg) where T: ParentClass, ChildClass 
{
    //do something
}

Однак цей код, як видається, argвиконує обов'язковість як типу, так ParentClass і ChildClass . Що я хочу зробити, це сказати, що аргумент може бути типом ParentClass або ChildClass таким чином:

public void test<T>(string a, T arg) where T: string OR Exception
{
//do something
}

Ваша допомога цінується як завжди!


4
Що ви могли б корисно зробити загальним способом у тілі такого методу (якщо декілька типів не походять від конкретного базового класу; у цьому випадку чому б не оголосити це обмеженням типу)?
Damien_The_Unbeliever

@Damien_The_Unbeliever Не знаєте, що ви маєте на увазі під тілом? Якщо ви не маєте на увазі дозвіл будь-якого типу та перевірку вручну в тілі ... та фактичному коді, який я хочу написати (останній фрагмент коду), я хотів би мати можливість передавати рядок АБО виняток, тому класу немає відносини там (крім system.object я уявляю).
Менсфілд

1
Також зауважте, що в письмовій формі немає ніякої користі where T : string, як stringце закритий клас. Єдине корисне, що ви могли зробити, - це визначити перевантаження stringі T : Exception, як пояснив @ Botz3000 у своїй відповіді нижче.
Маттіас Буеленс

Але коли немає відносин, єдиними методами, на які можна звернутися, argє ті, які визначаються object- так чому б не просто вилучити генеричні дані із зображення та зробити тип arg object? Що ти набираєш?
Damien_The_Unbeliever

1
@Mansfield Ви можете зробити приватний метод, який приймає параметр об'єкта. Обидві перевантаження називали б це. Тут не потрібні дженерики.
Botz3000

Відповіді:


68

Це неможливо. Однак ви можете визначити перевантаження для конкретних типів:

public void test(string a, string arg);
public void test(string a, Exception arg);

Якщо вони є частиною загального класу, вони віддаватимуть перевагу над загальним варіантом методу.


1
Цікаво. Це мені траплялося, але я думав, що це може зробити код чистішим для використання однієї функції. Ну добре, спасибі велике! Чи просто з цікавості ви знаєте, чи є конкретна причина, що це неможливо? (це свідомо лишилось з мови?)
Менсфілд

3
@ Менсфілд Я не знаю точної причини, але я думаю, що ти більше не зможеш використовувати загальні параметри змістовно. Всередині класу до них слід ставитися як до об'єкта, якби вони мали дозволити мати зовсім інші типи. Це означає, що ви можете так само випустити загальний параметр і забезпечити перевантаження.
Botz3000

3
@ Менсфілд, це тому, що orстосунки роблять речі далеко не загальними, щоб бути корисними. Вам доведеться задуматися, щоб зрозуміти, що робити і все таке. (ГИДОТА!).
Кріс Пфол

29

Відповідь Botz є 100% правильною, ось коротке пояснення:

Коли ви пишете метод (загальний чи ні) та оголошуєте типи параметрів, які приймає метод, визначаєте контракт:

Якщо ви дасте мені об'єкт, який вміє робити набір речей, які знає тип T, я можу доставити або "a": повернене значення типу, який я декларую, або "b": певну поведінку, яка використовує цього типу.

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

Якщо ви дасте мені предмет, який вміє стрибати на мотузці або знає, як обчислити пі до 15-ї цифри, я поверну або предмет, який може піти на риболовлю, або, можливо, змішати бетон.

Проблема полягає в тому, що, потрапляючи в метод, ви не маєте уявлення, чи дали вам це IJumpRopeчи то PiFactory. Крім того, коли ви йдете вперед і використовуєте метод (якщо припустити, що ви отримали магічну компіляцію), ви не впевнені, чи є у вас Fisherабо AbstractConcreteMixer. В основному це робить всю річ більш заплутаною.

Рішення вашої проблеми - одна з двох можливостей:

  1. Визначте більше одного методу, який визначає кожну можливу трансформацію, поведінку чи будь-яку іншу. Ось відповідь Боца. У світі програмування це називається методом перевантаження.

  2. Визначте базовий клас або інтерфейс, який вміє робити всі необхідні для методу речі, а один метод має саме цей тип. Це може включати в себе завершення stringі Exceptionв невеликому класі, щоб визначити, як ви плануєте зіставити їх для реалізації, але тоді все дуже зрозуміло і легко читати. Я міг би прийти через чотири роки і прочитати ваш код і легко зрозуміти, що відбувається.

Який ви обираєте, залежить від того, наскільки складним буде вибір 1 і 2, і наскільки він повинен бути розширеним.

Тож для вашої конкретної ситуації я думаю, що ви просто витягуєте повідомлення або щось із винятку:

public interface IHasMessage
{
    string GetMessage();
}

public void test(string a, IHasMessage arg)
{
    //Use message
}

Тепер все, що вам потрібно, - це методи, які перетворюють A stringі Exceptionна IHasMessage. Дуже легко.


Вибачте @ Botz3000, щойно помітив, що я неправильно написав ваше ім'я.
Кріс Пфол

Або компілятор може загрожувати типу як тип об'єднання в межах функції, і значення, що повертається, має той самий тип, що і він входить. TypeScript це робить.
Олексій

@ Алекс, але це не те, що робить C #.
Кріс Пфол

Це правда, але це могло. Я прочитав цю відповідь так, що вона не змогла б, я неправильно трактував?
Олексій

1
Річ у тім, що обмеження загальних параметрів C # і самі генерики є досить примітивними порівняно з, скажімо, шаблонами C ++. C # вимагає, щоб ви заздалегідь повідомили компілятору, які операції дозволені на загальних типах. Спосіб надання цієї інформації полягає в додаванні обмежень інтерфейсу реалізації (де T: IDisposable). Але ви, можливо, не хочете, щоб ваш тип реалізував деякий інтерфейс для використання загального методу, або ви можете дозволити деякі типи у вашому загальному коді, які не мають спільного інтерфейсу. Вих. дозволити будь-яку структуру або рядок, щоб ви могли просто викликати рівне (v1, v2) для порівняння на основі значень.
Вахтанг

8

Якщо ChildClass означає, що він походить від ParentClass, ви можете просто написати наступне, щоб прийняти і ParentClass, і ChildClass;

public void test<T>(string a, T arg) where T: ParentClass 
{
    //do something
}

З іншого боку, якщо ви хочете використовувати два різних типи без відношення спадкування між ними, вам слід розглянути типи, що реалізують один і той же інтерфейс;

public interface ICommonInterface
{
    string SomeCommonProperty { get; set; }
}

public class AA : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;set;
    }
}

public class BB : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;
        set;
    }
}

тоді ви можете записати свою загальну функцію як;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}

Хороша ідея, окрім того, я не думаю, що я зможу це зробити, оскільки хочу використовувати рядок, який є герметичним класом, згідно коментаря вище ...
Mansfield

це фактично проблема дизайну. загальна функція використовується для виконання подібних операцій з повторним використанням коду. обидва, якщо ви плануєте робити різні операції в тілі методу, роздільні методи - кращий спосіб (IMHO).
Дар’ял

Те, що я роблю, насправді - це написання простої функції реєстрації помилок. Я хотів би, щоб останній параметр був або рядком інформації про помилку, або винятком, і в будь-якому випадку я зберігаю e.message + e.stacktrace як рядок.
Менсфілд

Ви можете написати новий клас, маючи isSuccesful, зберегти повідомлення та виняток як властивості. тоді ви можете перевірити, чи справді видається справжнє, і виконати решту.
дариал

1

Стільки років, як це питання, я все ще отримую випадкові результати на моє пояснення вище. Пояснення все ще ідеально добре, як є, але я збираюся відповісти вдруге з типом, який мені добре послужив, як заміну для типів об'єднання (сильно набраний відповідь на питання, яке не підтримується безпосередньо C #, як це ).

using System;
using System.Diagnostics;

namespace Union {
    [DebuggerDisplay("{currType}: {ToString()}")]
    public struct Either<TP, TA> {
        enum CurrType {
            Neither = 0,
            Primary,
            Alternate,
        }
        private readonly CurrType currType;
        private readonly TP primary;
        private readonly TA alternate;

        public bool IsNeither => currType == CurrType.Primary;
        public bool IsPrimary => currType == CurrType.Primary;
        public bool IsAlternate => currType == CurrType.Alternate;

        public static implicit operator Either<TP, TA>(TP val) => new Either<TP, TA>(val);

        public static implicit operator Either<TP, TA>(TA val) => new Either<TP, TA>(val);

        public static implicit operator TP(Either<TP, TA> @this) => @this.Primary;

        public static implicit operator TA(Either<TP, TA> @this) => @this.Alternate;

        public override string ToString() {
            string description = IsNeither ? "" :
                $": {(IsPrimary ? typeof(TP).Name : typeof(TA).Name)}";
            return $"{currType.ToString("")}{description}";
        }

        public Either(TP val) {
            currType = CurrType.Primary;
            primary = val;
            alternate = default(TA);
        }

        public Either(TA val) {
            currType = CurrType.Alternate;
            alternate = val;
            primary = default(TP);
        }

        public TP Primary {
            get {
                Validate(CurrType.Primary);
                return primary;
            }
        }

        public TA Alternate {
            get {
                Validate(CurrType.Alternate);
                return alternate;
            }
        }

        private void Validate(CurrType desiredType) {
            if (desiredType != currType) {
                throw new InvalidOperationException($"Attempting to get {desiredType} when {currType} is set");
            }
        }
    }
}

Вищевказаний клас представляє тип, який може бути або TP, або TA. Ви можете використовувати його як такий (типи посилаються на мою оригінальну відповідь):

// ...
public static Either<FishingBot, ConcreteMixer> DemoFunc(Either<JumpRope, PiCalculator> arg) {
  if (arg.IsPrimary) {
    return new FishingBot(arg.Primary);
  }
  return new ConcreteMixer(arg.Secondary);
}

// elsewhere:

var fishBotOrConcreteMixer = DemoFunc(new JumpRope());
var fishBotOrConcreteMixer = DemoFunc(new PiCalculator());

Важливі примітки:

  • Ви отримаєте помилки під час виконання, якщо не перевірити IsPrimaryспочатку.
  • Ви можете перевірити будь-який із IsNeither IsPrimary або IsAlternate.
  • Ви можете отримати доступ до значення через PrimaryтаAlternate
  • Між TP / TA та Either є неявні перетворювачі, які дозволяють вам передавати значення або Eitherбудь-де, де очікується. Якщо ви робите пройти Eitherде TAабо TPяк очікується, алеEither містить неправильний тип значення , ви отримаєте повідомлення про помилку виконання.

Зазвичай я використовую це там, де хочу, щоб метод повернув результат або помилку. Це дійсно очищає код стилю. Я також дуже часто ( рідко ) використовую це як заміну методу перевантажень. Реально це дуже погана заміна такого перевантаження.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.