Type nullable comme paramètre générique possible?

287

Je veux faire quelque chose comme ça:

myYear = record.GetValueOrNull<int?>("myYear"),

Notez le type nullable comme paramètre générique.

Puisque la GetValueOrNullfonction pouvait retourner null, ma première tentative était la suivante:

public static T GetValueOrNull<T>(this DbDataRecord reader, string columnName)
  where T : class
{
    object columnValue = reader[columnName];

    if (!(columnValue is DBNull))
    {
        return (T)columnValue;
    }
    return null;
}

Mais l'erreur que je reçois maintenant est:

Le type 'int?' doit être un type de référence pour pouvoir l'utiliser comme paramètre 'T' dans le type ou la méthode générique

Droite! Nullable<int>est un struct! J'ai donc essayé de changer la contrainte de classe en structcontrainte (et comme effet secondaire, je ne peux plus retourner null):

public static T GetValueOrNull<T>(this DbDataRecord reader, string columnName)
  where T : struct

Maintenant l'affectation:

myYear = record.GetValueOrNull<int?>("myYear");

Donne l'erreur suivante:

Le type 'int?' doit être un type de valeur non nullable afin de l'utiliser comme paramètre 'T' dans le type ou la méthode générique

Est-il possible de spécifier un type nullable comme paramètre générique?

Tom Pester
la source
3
Pls pls faire votre signature IDataRecordde DbDataRecord..
nawfal

Réponses:

262

Modifiez le type de retour sur Nullable et appelez la méthode avec le paramètre non nullable

static void Main(string[] args)
{
    int? i = GetValueOrNull<int>(null, string.Empty);
}


public static Nullable<T> GetValueOrNull<T>(DbDataRecord reader, string columnName) where T : struct
{
    object columnValue = reader[columnName];

    if (!(columnValue is DBNull))
        return (T)columnValue;

    return null;
}
Greg Dean
la source
1
Je vous suggère d'utiliser "columnValue == DBNull.Value" au lieu de l'opérateur 'is', car son légèrement plus rapide =)
driAn
40
Préférence personnelle, mais vous pouvez utiliser le formulaire abrégé T? au lieu de Nullable <T>
Dunc
11
C'est bien pour les types de valeur, mais je pense que cela ne fonctionnera pas du tout avec les types de référence (par exemple GetValueOrNull <string>) parce que C # ne semble pas aimer Nullable <(ref type)> comme "string?". Les solutions de Robert C Barth & James Jones, ci-dessous, me semblent bien meilleures si tel est votre besoin.
bacar
2
@bacar - à droite, d'où le "where T: struct", si vous voulez des types de référence, vous pouvez créer une méthode similaire avec "where T: class"
Greg Dean
4
@Greg - bien sûr, mais alors vous avez besoin d'une deuxième méthode, et vous ne pouvez pas surcharger le nom. Comme je l'ai dit, si vous voulez gérer à la fois les types val et ref, je pense que des solutions plus propres sont présentées sur cette page.
bacar
107
public static T GetValueOrDefault<T>(this IDataRecord rdr, int index)
{
    object val = rdr[index];

    if (!(val is DBNull))
        return (T)val;

    return default(T);
}

Utilisez-le simplement comme ceci:

decimal? Quantity = rdr.GetValueOrDefault<decimal?>(1);
string Unit = rdr.GetValueOrDefault<string>(2);
James Jones
la source
6
Cela pourrait être raccourci comme suit: return rdr.IsDBNull (index)? par défaut (T): (T) rdr [index];
Foole
11
Je pense que cette question veut explicitement null , pas par défaut (T) .
mafu
5
@mafu default (T) renverra null pour les types de référence et 0 pour les types numériques, ce qui rend la solution plus flexible.
James Jones
2
Je pense qu'il est plus clair d'appeler cela GetValueOrDefaultpour préciser qu'il revient default(T)plutôt que null. Alternativement, vous pouvez lui faire lever une exception s'il Tn'est pas annulable.
Sam
Cette méthode présente de nombreux avantages et vous oblige à penser à renvoyer également autre chose que null.
Shane
61

Faites simplement deux choses avec votre code d'origine - supprimez la wherecontrainte et changez la dernière returnde return nullen return default(T). De cette façon, vous pouvez retourner le type que vous souhaitez.

Soit dit en passant, vous pouvez éviter l'utilisation de isen remplaçant votre ifrelevé par if (columnValue != DBNull.Value).

Robert C. Barth
la source
4
Cette solution ne fonctionne pas, car il y a une différence logique entre NULL et 0
Greg Dean
15
Cela fonctionne si le type qu'il passe est int?. Il renverra NULL, comme il le souhaite. S'il passe int comme type, il retournera 0 car un int ne peut pas être NULL. Outre le fait que je l'ai essayé et que cela fonctionne parfaitement.
Robert C.Barth
2
C'est la réponse la plus correcte et la plus flexible. Cependant, cela return defaultsuffit (vous n'avez pas besoin de (T), le compilateur le déduira du type de retour de signature).
McGuireV10
5

Avertissement: Cette réponse fonctionne, mais est destinée à des fins éducatives uniquement. :) La solution de James Jones est probablement la meilleure ici et certainement celle avec laquelle j'irais.

Le dynamicmot - clé de C # 4.0 rend cela encore plus facile, mais moins sûr:

public static dynamic GetNullableValue(this IDataRecord record, string columnName)
{
  var val = reader[columnName];

  return (val == DBNull.Value ? null : val);
}

Maintenant, vous n'avez pas besoin de l'indication de type explicite sur le RHS:

int? value = myDataReader.GetNullableValue("MyColumnName");

En fait, vous n'en avez même pas besoin du tout!

var value = myDataReader.GetNullableValue("MyColumnName");

value sera désormais un int, une chaîne ou tout autre type renvoyé par la base de données.

Le seul problème est que cela ne vous empêche pas d'utiliser des types non nullables sur le LHS, auquel cas vous obtiendrez une exception d'exécution plutôt méchante comme:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot convert null to 'int' because it is a non-nullable value type

Comme avec tout code qui utilise dynamic: caveat coder.

Ian Kemp
la source
4

Je devais juste faire quelque chose d'incroyable similaire à cela. Mon code:

public T IsNull<T>(this object value, T nullAlterative)
{
    if(value != DBNull.Value)
    {
        Type type = typeof(T);
        if (type.IsGenericType && 
            type.GetGenericTypeDefinition() == typeof(Nullable<>).GetGenericTypeDefinition())
        {
            type = Nullable.GetUnderlyingType(type);
        }

        return (T)(type.IsEnum ? Enum.ToObject(type, Convert.ToInt32(value)) :
            Convert.ChangeType(value, type));
    }
    else 
        return nullAlternative;
}
Toby
la source
3

Je pense que vous voulez gérer les types de référence et les types de struct. Je l'utilise pour convertir des chaînes d'élément XML en un type plus typé. Vous pouvez supprimer le nullAlternative avec réflexion. Le formatprovider doit gérer la culture dépendante "." ou «,» séparateur en par exemple décimales ou en pouces et doubles. Cela peut fonctionner:

public T GetValueOrNull<T>(string strElementNameToSearchFor, IFormatProvider provider = null ) 
    {
        IFormatProvider theProvider = provider == null ? Provider : provider;
        XElement elm = GetUniqueXElement(strElementNameToSearchFor);

        if (elm == null)
        {
            object o =  Activator.CreateInstance(typeof(T));
            return (T)o; 
        }
        else
        {
            try
            {
                Type type = typeof(T);
                if (type.IsGenericType &&
                type.GetGenericTypeDefinition() == typeof(Nullable<>).GetGenericTypeDefinition())
                {
                    type = Nullable.GetUnderlyingType(type);
                }
                return (T)Convert.ChangeType(elm.Value, type, theProvider); 
            }
            catch (Exception)
            {
                object o = Activator.CreateInstance(typeof(T));
                return (T)o; 
            }
        }
    }

Vous pouvez l'utiliser comme ceci:

iRes = helper.GetValueOrNull<int?>("top_overrun_length");
Assert.AreEqual(100, iRes);



decimal? dRes = helper.GetValueOrNull<decimal?>("top_overrun_bend_degrees");
Assert.AreEqual(new Decimal(10.1), dRes);

String strRes = helper.GetValueOrNull<String>("top_overrun_bend_degrees");
Assert.AreEqual("10.1", strRes);
Roland Roos
la source
2

Cela peut être un fil mort, mais j'ai tendance à utiliser ce qui suit:

public static T? GetValueOrNull<T>(this DbDataRecord reader, string columnName)
where T : struct 
{
    return reader[columnName] as T?;
}
Ryan Horch
la source
1
"Le type 'T' doit être un type de valeur non nullable afin de l'utiliser comme paramètre 'T' dans le type ou la méthode générique 'Nullable <T>'"
Ian Warburton
1

Je viens de rencontrer le même problème moi-même.

... = reader["myYear"] as int?; fonctionne et est propre.

Il fonctionne avec tout type sans problème. Si le résultat est DBNull, il renvoie null lorsque la conversion échoue.

Hele
la source
En fait, vous pourriez probablement faire int v=reader["myYear"]??-1;ou une autre valeur par défaut au lieu de -1. Cependant, cela pourrait soulever des problèmes si la valeur est DBNull...
nurchi
1

Je sais que c'est vieux, mais voici une autre solution:

public static bool GetValueOrDefault<T>(this SqlDataReader Reader, string ColumnName, out T Result)
{
    try
    {
        object ColumnValue = Reader[ColumnName];

        Result = (ColumnValue!=null && ColumnValue != DBNull.Value) ? (T)ColumnValue : default(T);

        return ColumnValue!=null && ColumnValue != DBNull.Value;
    }
    catch
    {
        // Possibly an invalid cast?
        return false;
    }
}

Maintenant, vous ne vous souciez plus de la Tvaleur ou du type de référence. Seulement si la fonction retourne true, vous avez une valeur raisonnable dans la base de données. Usage:

...
decimal Quantity;
if (rdr.GetValueOrDefault<decimal>("YourColumnName", out Quantity))
{
    // Do something with Quantity
}

Cette approche est très similaire à int.TryParse("123", out MyInt);

nurchi
la source
Ce serait bien si vous travailliez sur vos conventions de dénomination. Ils manquent de cohérence. À un endroit, il y a une variable sans capital puis il y en a une avec. La même chose avec les paramètres des méthodes.
Marino Šimić
1
Fait et fait! Le code Hope semble mieux maintenant. Bob est ta tante :) Tout est skookum
nurchi
0

Plusieurs contraintes génériques ne peuvent pas être combinées de manière OU (moins restrictive), mais uniquement de façon ET (plus restrictive). Cela signifie qu'une méthode ne peut pas gérer les deux scénarios. Les contraintes génériques ne peuvent pas non plus être utilisées pour créer une signature unique pour la méthode, vous devez donc utiliser 2 noms de méthode distincts.

Cependant, vous pouvez utiliser les contraintes génériques pour vous assurer que les méthodes sont utilisées correctement.

Dans mon cas, je voulais spécifiquement que null soit renvoyé, et jamais la valeur par défaut des types de valeurs possibles. GetValueOrDefault = mauvais. GetValueOrNull = bon.

J'ai utilisé les mots "Null" et "Nullable" pour faire la distinction entre les types de référence et les types de valeur. Et voici un exemple de quelques méthodes d'extension que j'ai écrites et qui complimentent la méthode FirstOrDefault dans la classe System.Linq.Enumerable.

    public static TSource FirstOrNull<TSource>(this IEnumerable<TSource> source)
        where TSource: class
    {
        if (source == null) return null;
        var result = source.FirstOrDefault();   // Default for a class is null
        return result;
    }

    public static TSource? FirstOrNullable<TSource>(this IEnumerable<TSource?> source)
        where TSource : struct
    {
        if (source == null) return null;
        var result = source.FirstOrDefault();   // Default for a nullable is null
        return result;
    }
Casey Plummer
la source
0

La manière la plus courte:

public static T ValueOrDefault<T>(this DataRow reader, string columnName) => 
        reader.IsNull(columnName) ? default : (T) reader[columnName];

revenir 0pour int, et nullpourint?

Amirhossein Yari
la source