Creando un método genérico en C #

84

Estoy tratando de combinar varios métodos similares en un método genérico. Tengo varios métodos que devuelven el valor de una cadena de consulta, o nulo si esa cadena de consulta no existe o no tiene el formato correcto. Esto sería bastante fácil si todos los tipos fueran anulables de forma nativa, pero tengo que usar el tipo genérico anulable para enteros y fechas.

Esto es lo que tengo ahora. Sin embargo, devolverá un 0 si un valor numérico no es válido y, lamentablemente, es un valor válido en mis escenarios. alguien me puede ayudar? ¡Gracias!

public static T GetQueryString<T>(string key) where T : IConvertible
{
    T result = default(T);

    if (String.IsNullOrEmpty(HttpContext.Current.Request.QueryString[key]) == false)
    {
        string value = HttpContext.Current.Request.QueryString[key];

        try
        {
            result = (T)Convert.ChangeType(value, typeof(T));  
        }
        catch
        {
            //Could not convert.  Pass back default value...
            result = default(T);
        }
    }

    return result;
}
Mike Cole
fuente
Está portando un montón de implementaciones, así que llame a la funcionalidad anterior, recuerde el resultado, llame a la nueva funcionalidad, recuerde el resultado, compare. Ahora haz eso 100 veces con un montón de entradas aleatorias, ¡y listo!
Hamish Grubijan
Lo siento, todavía no entiendo cómo se aplica eso en este caso. Todavía estoy tratando de que la función funcione.
Mike Cole
Al mirar las respuestas, estoy un poco confundido: ¿las personas que llaman están parametrizando usando int o int? como T?
Me parece que, en lugar de manejar esto internamente, debería permitir que el método arroje la excepción. Tal vez sea solo yo, pero alguien podría estar confundido por qué su llamada siempre devuelve el valor predeterminado, ya que no ven la excepción que se genera cuando ChangeTypefalla.
aplastar

Respuestas:

64

¿Qué pasa si especificaste el valor predeterminado para devolver, en lugar de usar el valor predeterminado (T)?

public static T GetQueryString<T>(string key, T defaultValue) {...}

También facilita las llamadas:

var intValue = GetQueryString("intParm", Int32.MinValue);
var strValue = GetQueryString("strParm", "");
var dtmValue = GetQueryString("dtmPatm", DateTime.Now); // eg use today's date if not specified

La desventaja es que necesita valores mágicos para indicar valores de cadena de consulta no válidos / faltantes.

Será
fuente
Sí, esto parece más viable que confiar en el valor predeterminado de un número entero. Lo tendré en cuenta. Todavía espero que mi función original funcione para todos los tipos, aunque puedo recurrir al uso de funciones no genéricas.
Mike Cole
¿Por qué no devolver algo distinto de cero para un entero no válido? Puede devolver cualquier cosa que desee que no sea un valor válido o que ya tenga un propósito especial, como nulo. Incluso podría crear crear su propio tipo llamado InvalidInteger o algo así. Está devolviendo nulo para una cadena de consulta incorrecta, ¿verdad? También podría devolver eso para un número entero no válido, por lo que nulo significaría simplemente 'algo está mal y no tengo ningún valor para usted', y tal vez pasar un código de razón por referencia a la función.
Dan Csharpster
1
Cómo obtener valor para: long ? testdonde el valor predeterminado debe ser nulo
Arshad
16

Lo sé, lo sé, pero ...

public static bool TryGetQueryString<T>(string key, out T queryString)
Arrendajo
fuente
4
El Try-pattern debe ser bien conocido por cualquier desarrollador de .NET. No es malo si me preguntas. En F # o NET 4.0, usaría Option (o Choice)
Christian Klauser
6
Si no es por otra razón, trato de evitarlo porque odio tener que "pre-declarar" esa variable de salida, especialmente si nunca se usa, un desperdicio de lo que de otra manera podría haber sido una línea de código perfectamente buena.
Jay
En realidad, es la forma más sencilla de resolver su problema: defina una función como la anterior + dos ayudantes que usarían esta función (esos serían 4 líneas).
greenoldman
1
Odio el patrón Try por la misma razón que dijo Jay. Preferiría una función genérica si fuera posible, que era mi objetivo original.
Mike Cole
11
Hazlo o no, ¡no hay ningún intento! <Yoda>
Conejo
12

¿Qué pasa con esto? Cambiar el tipo de retorno de TaNullable<T>

public static Nullable<T> GetQueryString<T>(string key) where T : struct, IConvertible
        {
            T result = default(T);

            if (String.IsNullOrEmpty(HttpContext.Current.Request.QueryString[key]) == false)
            {
                string value = HttpContext.Current.Request.QueryString[key];

                try
                {
                    result = (T)Convert.ChangeType(value, typeof(T));  
                }
                catch
                {
                    //Could not convert.  Pass back default value...
                    result = default(T);
                }
            }

            return result;
        }
Graviton
fuente
Error: el tipo 'T' debe ser un tipo de valor no anulable para poder usarlo como parámetro 'T' en el tipo o método genérico 'System.Nullable <T>'.
Mike Cole
También debe especificar where T : struct.
Aaronaught
@Mike C: No debería recibir el mismo error. El código editado definitivamente se compila.
Aaronaught
Sí, lo tengo ahora. Entonces, ¿qué sucede cuando quiero llamar a esto para el tipo String? No lo aceptará como está ahora.
Mike Cole
@MikeC, no creo que eso sea posible porque stringes un nullablevalor
Graviton
5

Puedes usar una especie de tal vez mónada (aunque prefiero la respuesta de Jay)

public class Maybe<T>
{
    private readonly T _value;

    public Maybe(T value)
    {
        _value = value;
        IsNothing = false;
    }

    public Maybe()
    {
        IsNothing = true;
    }

    public bool IsNothing { get; private set; }

    public T Value
    {
        get
        {
            if (IsNothing)
            {
                throw new InvalidOperationException("Value doesn't exist");
            }
            return _value;
        }
    }

    public override bool Equals(object other)
    {
        if (IsNothing)
        {
            return (other == null);
        }
        if (other == null)
        {
            return false;
        }
        return _value.Equals(other);
    }

    public override int GetHashCode()
    {
        if (IsNothing)
        {
            return 0;
        }
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        if (IsNothing)
        {
            return "";
        }
        return _value.ToString();
    }

    public static implicit operator Maybe<T>(T value)
    {
        return new Maybe<T>(value);
    }

    public static explicit operator T(Maybe<T> value)
    {
        return value.Value;
    }
}

Tu método se vería así:

    public static Maybe<T> GetQueryString<T>(string key) where T : IConvertible
    {
        if (String.IsNullOrEmpty(HttpContext.Current.Request.QueryString[key]) == false)
        {
            string value = HttpContext.Current.Request.QueryString[key];

            try
            {
                return (T)Convert.ChangeType(value, typeof(T));
            }
            catch
            {
                //Could not convert.  Pass back default value...
                return new Maybe<T>();
            }
        }

        return new Maybe<T>();
    }
Artem Govorov
fuente
4

Convert.ChangeType()no maneja correctamente tipos o enumeraciones que aceptan valores NULL en .NET 2.0 BCL (aunque creo que está arreglado para BCL 4.0). En lugar de hacer la implementación externa más compleja, haga que el convertidor haga más trabajo por usted. Aquí hay una implementación que uso:

public static class Converter
{
  public static T ConvertTo<T>(object value)
  {
    return ConvertTo(value, default(T));
  }

  public static T ConvertTo<T>(object value, T defaultValue)
  {
    if (value == DBNull.Value)
    {
      return defaultValue;
    }
    return (T) ChangeType(value, typeof(T));
  }

  public static object ChangeType(object value, Type conversionType)
  {
    if (conversionType == null)
    {
      throw new ArgumentNullException("conversionType");
    }

    // if it's not a nullable type, just pass through the parameters to Convert.ChangeType
    if (conversionType.IsGenericType && conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
    {
      // null input returns null output regardless of base type
      if (value == null)
      {
        return null;
      }

      // it's a nullable type, and not null, which means it can be converted to its underlying type,
      // so overwrite the passed-in conversion type with this underlying type
      conversionType = Nullable.GetUnderlyingType(conversionType);
    }
    else if (conversionType.IsEnum)
    {
      // strings require Parse method
      if (value is string)
      {
        return Enum.Parse(conversionType, (string) value);          
      }
      // primitive types can be instantiated using ToObject
      else if (value is int || value is uint || value is short || value is ushort || 
           value is byte || value is sbyte || value is long || value is ulong)
      {
        return Enum.ToObject(conversionType, value);
      }
      else
      {
        throw new ArgumentException(String.Format("Value cannot be converted to {0} - current type is " +
                              "not supported for enum conversions.", conversionType.FullName));
      }
    }

    return Convert.ChangeType(value, conversionType);
  }
}

Entonces su implementación de GetQueryString <T> puede ser:

public static T GetQueryString<T>(string key)
{
    T result = default(T);
    string value = HttpContext.Current.Request.QueryString[key];

    if (!String.IsNullOrEmpty(value))
    {
        try
        {
            result = Converter.ConvertTo<T>(value);  
        }
        catch
        {
            //Could not convert.  Pass back default value...
            result = default(T);
        }
    }

    return result;
}
Sam
fuente
0

Me gusta comenzar con una clase como esta configuración de clase {public int X {get; set;} public string Y {get; conjunto; } // repetir según sea necesario

 public settings()
 {
    this.X = defaultForX;
    this.Y = defaultForY;
    // repeat ...
 }
 public void Parse(Uri uri)
 {
    // parse values from query string.
    // if you need to distinguish from default vs. specified, add an appropriate property

 }

Esto ha funcionado bien en cientos de proyectos. Puede utilizar una de las muchas otras soluciones de análisis para analizar valores.

Sin reembolsos Sin devoluciones
fuente