Типовое ограничение множественного (ИЛИ) типа

135

Прочитав это , я узнал, что можно разрешить методу принимать параметры нескольких типов, сделав его универсальным методом. В этом примере следующий код используется с ограничением типа, чтобы гарантировать, что «U» является 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 . Я хочу сказать, что arg может быть типом ParentClass или ChildClass следующим образом:

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

Ваша помощь как всегда ценится!

Mansfield
источник
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);

Если они являются частью универсального класса, они будут предпочтительнее, чем универсальная версия метода.

Botz3000
источник
1
Интересный. Это произошло со мной, но я подумал, что это может сделать код чище, чтобы использовать одну функцию. Ах, хорошо, большое спасибо! Просто из любопытства, вы знаете, есть ли конкретная причина, по которой это невозможно? (было ли это намеренно исключено из языка?)
Мэнсфилд
3
@Mansfield Я не знаю точной причины, но думаю, что вы больше не сможете осмысленно использовать общие параметры. Внутри класса с ними нужно было бы обращаться как с объектами, если бы им было разрешено быть совершенно разных типов. Это означает, что вы могли бы просто опустить общий параметр и предоставить перегрузки.
Botz3000
3
@Mansfield, это потому, что orотношения делают вещи очень полезными. Вам придется поразмыслить, чтобы понять, что делать, и все такое. (YUCK!).
Крис Пфол,
29

Ответ Botz на 100% правильный, вот краткое объяснение:

Когда вы пишете метод (общий или нет) и объявляете типы параметров, которые принимает метод, вы определяете контракт:

Если вы дадите мне объект, который знает, как выполнять набор вещей, которые тип T знает, как это сделать, я могу передать либо «a»: возвращаемое значение типа, который я объявляю, либо «b»: какое-то поведение, которое использует этот тип.

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

Если вы дадите мне объект, который умеет прыгать через скакалку или знает, как вычислить число Пи до 15-й цифры, я верну либо объект, на котором можно ловить рыбу, либо, возможно, смешать бетон.

Проблема в том, что когда вы попадаете в метод, вы не представляете, дали ли они вам IJumpRopeили PiFactory. Более того, когда вы продолжаете использовать этот метод (при условии, что вы научились его волшебным образом скомпилировать), вы не совсем уверены, есть ли у вас a Fisherили AbstractConcreteMixer. По сути, это делает все это более запутанным.

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

  1. Определите более одного метода, который определяет каждое возможное преобразование, поведение или что-либо еще. Это ответ Ботца. В мире программирования это называется перегрузкой метода.

  2. Определите базовый класс или интерфейс, который знает, как делать все, что вам нужно для метода, и пусть один метод принимает только этот тип. Это может включать в себя упаковку stringи Exceptionв небольшой класс, чтобы определить, как вы планируете сопоставить их с реализацией, но тогда все будет очень ясно и легко читается. Я мог бы прийти через четыре года и прочесть твой код и легко понять, что происходит.

То, что вы выберете, зависит от того, насколько сложным будет выбор 1 и 2 и насколько он должен быть расширяемым.

Поэтому для вашей конкретной ситуации я собираюсь представить, что вы просто извлекаете сообщение или что-то из исключения:

public interface IHasMessage
{
    string GetMessage();
}

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

Теперь все, что вам нужно, это методы, которые преобразуют a stringи a Exceptionв IHasMessage. Очень просто.

Крис Пфол
источник
извините @ Botz3000, только что заметил, что я неправильно написал ваше имя.
Крис Пфол
Или компилятор может угрожать типу как типу объединения внутри функции и иметь возвращаемое значение того же типа, в который он входит. TypeScript делает это.
Алекс
@ Алекс, но это не то, что делает C #.
Крис Пфол
Это правда, но это возможно. Я читаю этот ответ как то, что он не сможет, я неправильно истолковал?
Alex
1
Дело в том, что ограничения универсальных параметров C # и сами шаблоны являются довольно примитивными по сравнению, скажем, с шаблонами C ++. C # требует, чтобы вы заранее сообщали компилятору, какие операции разрешены для универсальных типов. Способ предоставить эту информацию - добавить ограничение интерфейса реализаций (где T: IDisposable). Но вы можете не захотеть, чтобы ваш тип реализовывал некоторый интерфейс для использования универсального метода, или вы можете разрешить некоторые типы в вашем универсальном коде, которые не имеют общего интерфейса. Ex. разрешить любую структуру или строку, чтобы вы могли просто вызвать Equals (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
}
Дарьял
источник
Хорошая идея, за исключением того, что я не думаю, что смогу это сделать, поскольку я хочу использовать строку, которая является запечатанным классом в соответствии с комментарием выше ...
Мэнсфилд
это проблема дизайна на самом деле. общая функция используется для выполнения аналогичных операций с повторным использованием кода. если вы планируете выполнять различные операции в теле метода, лучше использовать разделение методов (IMHO).
Дарьял
На самом деле я пишу простую функцию регистрации ошибок. Я бы хотел, чтобы этот последний параметр был либо строкой информации об ошибке, либо исключением, и в этом случае я все равно сохраню e.message + e.stacktrace как строку.
Мэнсфилд
Вы можете написать новый класс, имеющий isSuccesful, сохранить сообщение и исключение в качестве свойств. тогда вы можете проверить, правильно ли выдано значение true, и сделать оставшееся.
Дарьял
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 содержит неправильный тип значения , вы получите сообщение об ошибке выполнения.

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

Крис Пфол
источник