Método genérico restricción de tipo múltiple (OR)

135

Al leer esto , aprendí que era posible permitir que un método aceptara parámetros de múltiples tipos al convertirlo en un método genérico. En el ejemplo, el siguiente código se usa con una restricción de tipo para garantizar que "U" sea un IEnumerable<T>.

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

Encontré más código que permitía agregar restricciones de tipo múltiple, como:

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

Sin embargo, este código parece imponer que argdebe ser un tipo de ParentClass y ChildClass . Lo que quiero hacer es decir que arg podría ser un tipo de ParentClass o ChildClass de la siguiente manera:

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

Su ayuda es apreciada como siempre!

Mansfield
fuente
44
¿Qué podría hacer útilmente, de manera genérica dentro del cuerpo de dicho método (a menos que todos los tipos múltiples se deriven de una clase base específica, en cuyo caso, ¿por qué no declarar eso como la restricción de tipo)?
Damien_The_Unbeliever
@Damien_The_Unbeliever ¿No está seguro de qué quiere decir en el cuerpo? A menos que se refiera a permitir cualquier tipo y verificar manualmente el cuerpo ... y en el código real que quiero escribir (el último fragmento de código), me gustaría poder pasar una cadena O excepción, por lo que no hay clase relación allí (excepto system.object me imagino).
Mansfield
1
También tenga en cuenta que no sirve de nada por escrito where T : string, ya que stringes una clase sellada . Lo único que podría hacer es definir las sobrecargas stringy T : Exception, como explica @ Botz3000 en su respuesta a continuación.
Mattias Buelens
Pero cuando no hay una relación, los únicos métodos a los que puede recurrir argson los definidos por object, entonces, ¿por qué no simplemente eliminar los genéricos de la imagen y hacer el tipo de arg object? Que estas ganando
Damien_The_Unbeliever
1
@Mansfield Puede crear un método privado que acepte un parámetro de objeto. Ambas sobrecargas llamarían a esa. No se necesitan genéricos aquí.
Botz3000

Respuestas:

68

Eso no es posible. Sin embargo, puede definir sobrecargas para tipos específicos:

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

Si son parte de una clase genérica, se preferirán a la versión genérica del método.

Botz3000
fuente
1
Interesante. Esto se me ocurrió, pero pensé que podría hacer que el código fuera más limpio para usar una función. Ah bueno, muchas gracias! Solo por curiosidad, ¿sabes si hay una razón específica por la que esto no es posible? (¿se ha dejado fuera del idioma deliberadamente?)
Mansfield
3
@Mansfield No sé la razón exacta, pero creo que ya no podrás usar los parámetros genéricos de manera significativa. Dentro de la clase, tendrían que ser tratados como un objeto si se les permitiera ser de tipos completamente diferentes. Eso significa que también podría omitir el parámetro genérico y proporcionar sobrecargas.
Botz3000
3
@Mansfield, es porque una orrelación hace que las cosas, en general, sean útiles. Te tiene que hacer la reflexión de averiguar qué hacer y todo eso. (¡YUCK!)
Chris Pfohl
29

La respuesta de Botz es 100% correcta, aquí hay una breve explicación:

Cuando escribe un método (genérico o no) y declara los tipos de parámetros que toma el método, está definiendo un contrato:

Si me das un objeto que sabe cómo hacer el conjunto de cosas que el Tipo T sabe cómo hacer, puedo entregar 'a': un valor de retorno del tipo que declaro, o 'b': algún tipo de comportamiento que utiliza ese tipo.

Si intenta darle más de un tipo a la vez (con un o) o intenta que devuelva un valor que podría ser más de un tipo, el contrato se vuelve borroso:

Si me das un objeto que sepa saltar la cuerda o que calcule pi hasta el décimo quinto dígito, devolveré un objeto que puede ir a pescar o tal vez mezclar concreto.

El problema es que cuando entras en el método no tienes idea si te han dado un IJumpRopeo un PiFactory. Además, cuando continúas y usas el método (suponiendo que lo has compilado mágicamente) no estás realmente seguro de tener un Fishero un AbstractConcreteMixer. Básicamente hace que todo sea mucho más confuso.

La solución a su problema es una de dos posibilidades:

  1. Defina más de un método que defina cada posible transformación, comportamiento o lo que sea. Esa es la respuesta de Botz. En el mundo de la programación, esto se conoce como sobrecarga del método.

  2. Defina una clase base o interfaz que sepa cómo hacer todas las cosas que necesita para el método y haga que un método tome solo ese tipo. Esto puede implicar envolver una stringy Exceptionen una pequeña clase para definir cómo se planea en el mapeo de ellos para la puesta en práctica, pero luego todo es super clara y fácil de leer. Podría venir, dentro de cuatro años, leer su código y comprender fácilmente lo que está sucediendo.

El que elija depende de cuán complicadas serían las opciones 1 y 2 y de lo extensible que debe ser.

Entonces, para su situación específica, voy a imaginar que solo está sacando un mensaje o algo de la excepción:

public interface IHasMessage
{
    string GetMessage();
}

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

Ahora todo lo que necesita son métodos que transforman una stringy una Exceptiona una IHasMessage. Muy fácil.

Chris Pfohl
fuente
lo siento @ Botz3000, acabo de notar que escribí mal tu nombre.
Chris Pfohl
O el compilador podría amenazar el tipo como un tipo de unión dentro de la función y hacer que el valor de retorno sea del mismo tipo que ingresa. TypeScript lo hace.
Alex
@ Alex, pero eso no es lo que hace C #.
Chris Pfohl
Eso es cierto, pero podría. Leí esta respuesta como si no fuera capaz, ¿interpreté mal?
Alex
1
La cuestión es que las restricciones de parámetros genéricos de C # y los genéricos en sí mismos son bastante primitivos en comparación con, por ejemplo, las plantillas de C ++. C # requiere que le digas al compilador por adelantado qué operaciones están permitidas en los tipos genéricos. La forma de proporcionar esa información es agregar una restricción de interfaz de implementos (donde T: IDisposable). Pero es posible que no desee que su tipo implemente alguna interfaz para usar un método genérico o puede permitir algunos tipos en su código genérico que no tengan una interfaz común. Ex. permita cualquier estructura o cadena para que simplemente pueda llamar a Equals (v1, v2) para una comparación basada en valores.
Vakhtang
8

Si ChildClass significa que se deriva de ParentClass, puede escribir lo siguiente para aceptar ParentClass y ChildClass;

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

Por otro lado, si desea utilizar dos tipos diferentes sin relación de herencia entre ellos, debe considerar los tipos que implementan la misma interfaz;

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

entonces puedes escribir tu función genérica como;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}
daryal
fuente
Buena idea, excepto que no creo que pueda hacer esto ya que quiero usar una cadena que es una clase sellada según un comentario anterior ...
Mansfield
Este es un problema de diseño de hecho. Se utiliza una función genérica para realizar operaciones similares con la reutilización de código. Si planea realizar diferentes operaciones en el cuerpo del método, separar métodos es una mejor manera (en mi humilde opinión).
daryal
Lo que estoy haciendo, de hecho, es escribir una simple función de registro de errores. Me gustaría que ese último parámetro sea una cadena de información sobre el error, o una excepción, en cuyo caso guardo e.message + e.stacktrace como una cadena de todos modos.
Mansfield
puede escribir una nueva clase, teniendo isSuccesful, guardar mensaje y excepción como propiedades. entonces puede verificar si el resultado es verdadero y hacer el resto.
daryal
1

Tan antigua como esta pregunta es, todavía recibo votos al azar sobre mi explicación anterior. La explicación sigue siendo perfectamente correcta, pero voy a responder por segunda vez con un tipo que me sirvió como un sustituto para los tipos de unión (la respuesta fuertemente tipada a la pregunta que C # no admite directamente como es). )

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

La clase anterior representa un tipo que puede ser cualquiera de TP o TA. Puede usarlo como tal (los tipos se refieren a mi respuesta 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());

Notas importantes:

  • Obtendrá errores de tiempo de ejecución si no verifica IsPrimaryprimero.
  • Puedes consultar cualquiera de IsNeither IsPrimaryo IsAlternate.
  • Puede acceder al valor a través de PrimaryyAlternate
  • Hay convertidores implícitos entre TP / TA y O para permitirle pasar los valores o Eithercualquier lugar donde se espera uno. Si se hace pasar una Eitheren una TA, o TPse espera, pero la Eithercontiene el tipo incorrecto de valor que obtendrá un error de ejecución.

Normalmente uso esto donde quiero que un método devuelva un resultado o un error. Realmente limpia ese código de estilo. También ocasionalmente ( rara vez ) uso esto como un reemplazo para las sobrecargas de métodos. Siendo realistas, este es un sustituto muy pobre para tal sobrecarga.

Chris Pfohl
fuente