Restrição de tipo genérico de método múltiplo (OR)

135

Ao ler isso , aprendi que era possível permitir que um método aceitasse parâmetros de vários tipos, tornando-o um método genérico. No exemplo, o código a seguir é usado com uma restrição de tipo para garantir que "U" seja um IEnumerable<T>.

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

Encontrei um pouco mais de código que permitia adicionar várias restrições de tipo, como:

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

No entanto, esse código parece impor que argdeve ser um tipo de ParentClass e ChildClass . O que eu quero fazer é dizer que arg pode ser um tipo de ParentClass ou ChildClass da seguinte maneira:

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

Sua ajuda é apreciada como sempre!

Mansfield
fonte
4
O que você poderia fazer de uma maneira genérica no corpo de um método desse tipo (a menos que todos os tipos múltiplos derivem de uma classe base específica; nesse caso, por que não declarar isso como restrição de tipo)?
Damien_The_Unbeliever
@Damien_The_Unbeliever Não sabe o que quer dizer com no corpo? A menos que você queira permitir qualquer tipo e verificar manualmente o corpo ... e no código real que eu quero escrever (o último trecho de código), gostaria de poder passar uma string OU exceção, para que não haja classe relação lá (exceto system.object eu imagino).
Mansfield
1
Observe também que não há uso por escrito where T : string, como stringé uma classe selada . A única coisa útil que você pode fazer é definir sobrecargas stringe T : Exception, como explicado por @ Botz3000 em sua resposta abaixo.
Mattias Buelens
Mas quando não há relacionamento, os únicos métodos que você pode chamar argsão os definidos por object- então, por que não remover os genéricos da imagem e fazer o tipo arg object? O que você está ganhando?
Damien_The_Unbeliever
1
@Mansfield Você pode criar um método privado que aceite um parâmetro de objeto. Ambas as sobrecargas chamariam isso. Não são necessários genéricos aqui.
Botz3000

Respostas:

68

Isso não é possível. No entanto, você pode definir sobrecargas para tipos específicos:

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

Se eles fazem parte de uma classe genérica, serão preferidos à versão genérica do método.

Botz3000
fonte
1
Interessante. Isso me ocorreu, mas achei que poderia tornar o código mais limpo para usar uma função. Ah, bem, muito obrigado! Por curiosidade, você sabe se existe um motivo específico para isso não ser possível? (ele tem sido deixado de fora da linguagem deliberadamente?)
Mansfield
3
@ Morganfield Eu não sei o motivo exato, mas acho que você não seria capaz de usar os parâmetros genéricos de maneira significativa. Dentro da classe, eles teriam que ser tratados como objeto se tivessem permissão de serem de tipos completamente diferentes. Isso significa que você também pode deixar de fora o parâmetro genérico e fornecer sobrecargas.
Botz3000
3
@ Morganfield, é porque um orrelacionamento faz com que as coisas em geral sejam úteis. Você iria ter que fazer reflexão para descobrir o que fazer e tudo isso. (QUE NOJO!).
Chris Pfohl
29

A resposta de Botz é 100% correta, aqui está uma breve explicação:

Ao escrever um método (genérico ou não) e declarar os tipos de parâmetros que o método utiliza, você define um contrato:

Se você me der um objeto que saiba fazer o que o Tipo T sabe fazer, posso fornecer 'a': um valor de retorno do tipo que declaro, ou 'b': algum tipo de comportamento que usa esse tipo.

Se você tentar atribuir mais de um tipo por vez (com um ou) ou tentar fazer com que retorne um valor que pode ser mais de um tipo que o contrato fica confuso:

Se você me der um objeto que saiba pular corda ou saiba calcular pi até o 15º dígito, retornarei um objeto que possa pescar ou talvez misturar concreto.

O problema é que, quando você entra no método, não tem idéia se eles lhe deram um IJumpRopeou um PiFactory. Além disso, quando você segue em frente e usa o método (supondo que você o tenha compilado magicamente), você não tem certeza se possui um Fisherou um AbstractConcreteMixer. Basicamente, torna tudo mais confuso.

A solução para o seu problema é uma das duas possibilidades:

  1. Defina mais de um método que define cada possível transformação, comportamento ou qualquer outra coisa. Essa é a resposta de Botz. No mundo da programação, isso é conhecido como Sobrecarregando o método.

  2. Defina uma classe base ou interface que saiba como fazer todas as coisas necessárias para o método e faça com que um método use exatamente esse tipo. Isso pode envolver agrupar uma classe stringe Exceptionuma classe pequena para definir como você planeja mapeá-las para a implementação, mas tudo é super claro e fácil de ler. Eu poderia vir daqui a quatro anos e ler seu código e entender facilmente o que está acontecendo.

Qual você escolhe depende de quão complicada a escolha 1 e 2 seria e quão extensível ela precisa ser.

Então, para a sua situação específica, vou imaginar que você está apenas recebendo uma mensagem ou algo da exceção:

public interface IHasMessage
{
    string GetMessage();
}

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

Agora, tudo o que você precisa são métodos que transformam um stringe um Exceptionem um IHasMessage. Muito fácil.

Chris Pfohl
fonte
desculpe @ Botz3000, só notei que eu escrevi errado o seu nome.
precisa saber é o seguinte
Ou o compilador pode ameaçar o tipo como um tipo de união dentro da função e fazer com que o valor de retorno seja do mesmo tipo que entra. O TypeScript faz isso.
Alex
@ Alex, mas não é isso que C # faz.
precisa saber é o seguinte
Isso é verdade, mas poderia. Eu li essa resposta como se não fosse capaz, interpretei errado?
Alex
1
O problema é que as restrições genéricas de parâmetro e os genéricos do C # são bastante primitivos em comparação com, digamos, modelos C ++. O C # exige que você informe ao compilador antecipadamente quais operações são permitidas em tipos genéricos. A maneira de fornecer essas informações é adicionar uma restrição de interface de implementos (onde T: IDisposable). Mas você pode não querer que seu tipo implemente alguma interface para usar um método genérico ou permitir que alguns tipos em seu código genérico não possuam interface comum. Ex. permita qualquer estrutura ou sequência para que você possa simplesmente chamar Equals (v1, v2) para comparação baseada em valor.
Vakhtang
8

Se ChildClass significa que é derivado de ParentClass, você pode apenas escrever o seguinte para aceitar ParentClass e ChildClass;

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

Por outro lado, se você quiser usar dois tipos diferentes sem nenhuma relação de herança entre eles, considere os tipos que implementam a mesma interface;

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

então você pode escrever sua função genérica como;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}
daryal
fonte
Boa idéia, só que eu não acho que vou ser capaz de fazer isso como eu quero usar corda que é uma classe selada de acordo com um comentário anterior ...
Mansfield
de fato, esse é um problema de design. uma função genérica é usada para executar operações semelhantes com a reutilização de código. Se você planeja realizar operações diferentes no corpo do método, separar os métodos é uma maneira melhor (IMHO).
Daryal
Na verdade, o que estou fazendo é escrever uma função simples de registro de erros. Gostaria que esse último parâmetro fosse uma sequência de informações sobre o erro ou uma exceção. Nesse caso, salve e.message + e.stacktrace como uma sequência de qualquer maneira.
Mansfield
você pode escrever uma nova classe, tendo isSuccesful, salve a mensagem e a exceção como propriedades. então você pode verificar se verdadeiro é verdadeiro e fazer o restante.
Daryal
1

Por mais antiga que seja essa pergunta, ainda recebo votos aleatórios na minha explicação acima. A explicação ainda está perfeitamente bem, mas vou responder uma segunda vez com um tipo que me serviu bem como substituto para tipos de união (a resposta fortemente tipada para a pergunta que não é diretamente suportada pelo C #, como é )

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

A classe acima representa um tipo que pode ser quer TP ou TA. Você pode usá-lo como tal (os tipos se referem à minha resposta original):

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

Anotações importantes:

  • Você receberá erros de tempo de execução se não verificar IsPrimaryprimeiro.
  • Você pode verificar qualquer um IsNeither IsPrimaryou IsAlternate.
  • Você pode acessar o valor por meio de PrimaryeAlternate
  • Existem conversores implícitos entre TP / TA e E para permitir a passagem dos valores ou para Eitherqualquer lugar em que seja esperado. Se você fazer passar um Eitheronde um TAou TPé esperado, mas o Eithercontém o tipo errado de valor que você vai receber um erro de execução.

Normalmente, uso isso onde quero que um método retorne um resultado ou um erro. Realmente limpa esse código de estilo. Eu também muito ocasionalmente ( raramente ) uso isso como um substituto para sobrecargas de método. Realisticamente, esse é um substituto muito ruim para essa sobrecarga.

Chris Pfohl
fonte