Jaki jest właściwy sposób sprawdzania wartości null?

122

Uwielbiam operator łączenia wartości null, ponieważ ułatwia przypisywanie wartości domyślnej dla typów dopuszczających wartość null.

 int y = x ?? -1;

To świetnie, z wyjątkiem sytuacji, gdy muszę zrobić coś prostego x. Na przykład, jeśli chcę to sprawdzić Session, zwykle muszę napisać coś bardziej szczegółowego.

Chciałbym móc to zrobić:

string y = Session["key"].ToString() ?? "none";

Ale nie możesz, ponieważ .ToString()zostaje wywołany przed sprawdzeniem zerowej wartości, więc kończy się niepowodzeniem, jeśli Session["key"]jest zerowy. W końcu robię to:

string y = Session["key"] == null ? "none" : Session["key"].ToString();

Działa i moim zdaniem jest lepsza niż trzywierszowa alternatywa:

string y = "none";
if (Session["key"] != null)
    y = Session["key"].ToString();

Mimo że to działa, wciąż jestem ciekawy, czy istnieje lepszy sposób. Wydaje się, że nie ma znaczenia, do czego zawsze muszę się odwoływać Session["key"]dwukrotnie; raz do sprawdzenia i jeszcze raz do zadania. Jakieś pomysły?

Chev
źródło
20
To wtedy chciałbym, żeby C # miał „bezpiecznego operatora nawigacji” ( .?), jak ma Groovy .
Cameron
2
@Cameron: To jest, gdy chciałbym, aby C # mógł traktować typy dopuszczające wartość null (w tym typy referencyjne) jako monadę, więc nie potrzebujesz „bezpiecznego operatora nawigacji”.
Jon Purdy
3
Twórca zerowych odniesień nazwał to swoim „miliardowym błędem” i zwykle się z tym zgadzam. Zobacz infoq.com/presentations/…
Jamie Ide
Jego faktycznym błędem jest niebezpieczne (nie wymuszane językiem) mieszanie typów dopuszczających wartość null i nie-nullabelowych.
MSalters
@JamieIde Dzięki za bardzo ciekawy link. :)
BobRodes

Odpowiedzi:

182

Co powiesz na

string y = (Session["key"] ?? "none").ToString();
Czarny niedźwiedź
źródło
79
Siła jest mocna z tym.
Chev
2
@Matthew: Nie, ponieważ wartości Session są typu Object
BlackBear
1
@BlackBear, ale zwrócona wartość jest najprawdopodobniej ciągiem, więc rzutowanie jest prawidłowe
Firo
To była najbardziej bezpośrednia odpowiedź na moje pytanie, więc zaznaczam odpowiedź, ale metoda rozszerzania Jona Skeeta .ToStringOrDefault()jest moim preferowanym sposobem robienia tego. Jednak używam tej odpowiedzi w ramach metody rozszerzenia Jona;)
Chev
10
Nie podoba mi się to, ponieważ jeśli masz w sesji inny typ obiektu niż ten, którego się spodziewasz, możesz ukrywać pewne subtelne błędy w swoim programie. Wolałbym raczej użyć bezpiecznego rzutu, ponieważ myślę, że prawdopodobnie szybciej wykryje błędy. Unika również wywoływania ToString () na obiekcie ciągu.
tvanfosson
130

Jeśli często robisz to specjalnie z,ToString() możesz napisać metodę rozszerzającą:

public static string NullPreservingToString(this object input)
{
    return input == null ? null : input.ToString();
}

...

string y = Session["key"].NullPreservingToString() ?? "none";

Lub oczywiście domyślna metoda:

public static string ToStringOrDefault(this object input, string defaultValue)
{
    return input == null ? defaultValue : input.ToString();
}

...

string y = Session["key"].ToStringOrDefault("none");
Jon Skeet
źródło
16
.ToStringOrDefault()jest prosty i elegancki. Fajne rozwiązanie.
Chev
7
W ogóle nie mogę się z tym zgodzić. Metody rozszerzające objectsą przekleństwem i śmieciami w bazie kodu, a metody rozszerzające, które działają bez błędów na thiswartościach null , są czystym złem.
Nick Larsen
10
@NickLarsen: Wszystko z umiarem, mówię. Metody rozszerzeń, które działają z wartością null, mogą być bardzo przydatne, IMO - o ile mają jasność co do tego, co robią.
Jon Skeet
3
@ one.beat.consumer: Tak. Gdyby to było tylko formatowanie (lub jakiekolwiek literówki), to byłaby jedna rzecz, ale zmiana wybranej przez autora nazwy metody wykracza poza normalnie odpowiednią edycję, IMO.
Jon Skeet,
6
@ one.beat.consumer: Kiedy poprawiasz gramatykę i literówki, to w porządku - ale zmiana imienia, które ktoś (kogokolwiek, nie tylko ja) wybrał wyraźnie celowo, wydaje mi się inna. W tym miejscu sugerowałbym to w komentarzu.
Jon Skeet
21

Możesz także użyć as, co daje, nulljeśli konwersja się nie powiedzie:

Session["key"] as string ?? "none"

To wróci "none"nawet jeśli ktoś nadziewane się intin Session["key"].

Andomar
źródło
1
Działa to tylko wtedy, gdy nie potrzebujesz ToString()w pierwszej kolejności.
Abel
1
Dziwię się, że nikt jeszcze nie odrzucił tej odpowiedzi. Jest to całkowicie odmienne semantycznie od tego, co chce zrobić PO.
Timwi
@Timwi: OP używa ToString()do konwersji obiektu zawierającego ciąg znaków na ciąg. Możesz zrobić to samo za pomocą obj as stringlub (string)obj. To dość powszechna sytuacja w ASP.NET.
Andomar
5
@Andomar: Nie, OP wywołuje ToString()obiekt (mianowicie Session["key"]), którego typu nie wymienił. Może to być dowolny obiekt, niekoniecznie ciąg.
Timwi
13

Jeśli zawsze będzie string, możesz przesyłać:

string y = (string)Session["key"] ?? "none";

Ma to tę zaletę, że narzekają, zamiast ukrywać błąd, jeśli ktoś wbija intcoś lub coś w środku Session["key"]. ;)

Ry-
źródło
10

Wszystkie sugerowane rozwiązania są dobre i odpowiedz na pytanie; więc to jest tylko nieznaczne wydłużenie. Obecnie większość odpowiedzi dotyczy tylko walidacji wartości null i typów ciągów. Możesz rozszerzyć StateBagobiekt, aby zawierał ogólny plikGetValueOrDefault metodę , podobną do odpowiedzi opublikowanej przez Jona Skeeta.

Prosta ogólna metoda rozszerzenia, która akceptuje ciąg jako klucz, a następnie typ sprawdza obiekt sesji. Jeśli obiekt ma wartość null lub nie jest tego samego typu, zwracana jest wartość domyślna, w przeciwnym razie zwracana jest wartość sesji o silnym typie.

Coś takiego

/// <summary>
/// Gets a value from the current session, if the type is correct and present
/// </summary>
/// <param name="key">The session key</param>
/// <param name="defaultValue">The default value</param>
/// <returns>Returns a strongly typed session object, or default value</returns>
public static T GetValueOrDefault<T>(this HttpSessionState source, string key, T defaultValue)
{
    // check if the session object exists, and is of the correct type
    object value = source[key]
    if (value == null || !(value is T))
    {
        return defaultValue;
    }

    // return the session object
    return (T)value;
}
Richard
źródło
1
Czy możesz dołączyć próbkę użycia dla tej metody rozszerzenia? Czy StateBag nie zajmuje się stanem widoku, a nie sesją? Używam ASP.NET MVC 3, więc nie mam prostego dostępu do widoku stanu. Myślę, że chcesz przedłużyć HttpSessionState.
Chev
ta odpowiedź wymaga pobrania wartości 3x i 2 rzutów, jeśli się powiedzie. (wiem, że jest to słownik, ale początkujący mogą korzystać z podobnych praktyk na drogich metod.)
Jake Berger
3
T value = source[key] as T; return value ?? defaultValue;
Jake Berger,
1
@jberger Rzutowanie na wartość przy użyciu „as” jest niedostępne, ponieważ nie ma ograniczenia klasy dla typu ogólnego, ponieważ potencjalnie możesz chcieć zwrócić wartość, taką jak bool. @AlexFord Przepraszam, chciałbyś przedłużyć HttpSessionStatesesję. :)
Richard
w rzeczy samej. jak zauważył Richard, wymaga ograniczenia. (... i inna metoda, jeśli chcesz użyć typów wartości)
Jake Berger
7

Używamy metody o nazwie NullOr.

Stosowanie

// Call ToString() if it’s not null, otherwise return null
var str = myObj.NullOr(obj => obj.ToString());

// Supply default value for when it’s null
var str = myObj.NullOr(obj => obj.ToString()) ?? "none";

// Works with nullable return values, too —
// this is properly typed as “int?” (nullable int)
// even if “Count” is just int
var count = myCollection.NullOr(coll => coll.Count);

// Works with nullable input types, too
int? unsure = 47;
var sure = unsure.NullOr(i => i.ToString());

Źródło

/// <summary>Provides a function delegate that accepts only value types as return types.</summary>
/// <remarks>This type was introduced to make <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncStruct{TInput,TResult})"/>
/// work without clashing with <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncClass{TInput,TResult})"/>.</remarks>
public delegate TResult FuncStruct<in TInput, TResult>(TInput input) where TResult : struct;

/// <summary>Provides a function delegate that accepts only reference types as return types.</summary>
/// <remarks>This type was introduced to make <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncClass{TInput,TResult})"/>
/// work without clashing with <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncStruct{TInput,TResult})"/>.</remarks>
public delegate TResult FuncClass<in TInput, TResult>(TInput input) where TResult : class;

/// <summary>Provides extension methods that apply to all types.</summary>
public static class ObjectExtensions
{
    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult NullOr<TInput, TResult>(this TInput input, FuncClass<TInput, TResult> lambda) where TResult : class
    {
        return input == null ? null : lambda(input);
    }

    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult? NullOr<TInput, TResult>(this TInput input, Func<TInput, TResult?> lambda) where TResult : struct
    {
        return input == null ? null : lambda(input);
    }

    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult? NullOr<TInput, TResult>(this TInput input, FuncStruct<TInput, TResult> lambda) where TResult : struct
    {
        return input == null ? null : lambda(input).Nullable();
    }
}
Timwi
źródło
Tak, to jest bardziej ogólna odpowiedź na zamierzony problem - pokonałeś mnie w tym - i kandydat do bezpiecznej nawigacji (jeśli nie masz nic przeciwko lambda dla prostych rzeczy) - ale nadal jest to trochę kłopotliwe, dobrze :). Osobiście zawsze bym wybierał? : zamiast tego (jeśli nie drogie, jeśli to i tak to przestawić) ...
NSGaga-głównie-nieaktywny
... A 'Nazewnictwo' jest prawdziwym problemem z tym - nic tak naprawdę nie wydaje się przedstawiać dobrze (ani 'dodaje' za dużo) lub jest długie - NullOr jest dobre, ale zbyt duży nacisk na 'zerowy' IMO (plus ty have ?? still) - użyłem „Property” lub „Safe”. value.Dot (o => o.property) ?? @default może?
NSGaga-głównie-nieaktywny
@NSGaga: Od dłuższego czasu pracowaliśmy nad nazwą. DotRozważaliśmy, ale uznaliśmy, że jest to zbyt nieopisowe. Zdecydowaliśmy się na NullOrdobry kompromis między samowyjaśnieniem a zwięzłością. Jeśli naprawdę nie dbałeś o nazewnictwo, zawsze możesz to nazwać _. Jeśli uznasz, że lambdy są zbyt kłopotliwe do napisania, możesz użyć do tego fragmentu kodu, ale osobiście uważam to za dość łatwe. Co do tego ? :, nie możesz tego używać z bardziej złożonymi wyrażeniami, musiałbyś przenieść je do nowego lokalnego; NullOrpozwala tego uniknąć.
Timwi
6

Wolałbym jednorazowo użyć bezpiecznego rzutowania na łańcuch na wypadek, gdyby obiekt przechowywany z kluczem nie był jednym. Używanie ToString()może nie przynieść oczekiwanych rezultatów.

var y = Session["key"] as string ?? "none";

Jak mówi @Jon Skeet, jeśli często robisz to jako metoda rozszerzająca lub, jeszcze lepiej, może metoda rozszerzająca w połączeniu z silnie wpisaną klasą SessionWrapper. Nawet bez metody rozszerzającej opakowanie o jednoznacznie określonym typie może być dobrym pomysłem.

public class SessionWrapper
{
    private HttpSessionBase Session { get; set; }

    public SessionWrapper( HttpSessionBase session )
    {
        Session = session;
    }

    public SessionWrapper() : this( HttpContext.Current.Session ) { }

    public string Key
    {
         get { return Session["key"] as string ?? "none";
    }

    public int MaxAllowed
    {
         get { return Session["maxAllowed"] as int? ?? 10 }
    }
}

Użyty jako

 var session = new SessionWrapper(Session);

 string key = session.Key;
 int maxAllowed = session.maxAllowed;
tvanfosson
źródło
3

stworzyć funkcję pomocniczą

public static String GetValue( string key, string default )
{
    if ( Session[ key ] == null ) { return default; }
    return Session[ key ].toString();
}


string y = GetValue( 'key', 'none' );
scibuff
źródło
2

Odpowiedź Skeeta jest najlepsza - szczególnie uważam, że jego ToStringOrNull()jest dość elegancki i najlepiej pasuje do twoich potrzeb. Chciałem dodać jeszcze jedną opcję do listy metod rozszerzających:

Zwróć oryginalny obiekt lub domyślną wartość ciągu dla null :

// Method:
public static object OrNullAsString(this object input, string defaultValue)
{
    if (defaultValue == null)
        throw new ArgumentNullException("defaultValue");
    return input == null ? defaultValue : input;
}

// Example:
var y = Session["key"].OrNullAsString("defaultValue");

Użyj vardla zwróconej wartości, ponieważ wróci jako typ oryginalnego wejścia, tylko jako ciąg domyślny, kiedynull

one.beat.consumer
źródło
Po co rzucać wyjątek, null defaultValuejeśli nie jest potrzebny (to znaczy input != null)?
Attila
input != nullEval zwróci przedmiot jako siebie. input == nullzwraca ciąg podany jako parametr. dlatego jest możliwe, że osoba mogłaby zadzwonić .OnNullAsString(null)- ale celem (choć rzadko użyteczna metoda rozszerzenia) było zapewnienie albo odzyskania obiektu z powrotem, albo domyślnego ciągu ... nigdy null
one.beat.consumer
input!=nullScenariusz zwróci jedynie wejście czy defaultValue!=nullteż posiada; w przeciwnym razie wyrzuci plik ArgumentNullException.
Attila
0

To jest mój mały "operator Elvisa" bezpieczny dla typów dla wersji .NET, które nie obsługują?.

public class IsNull
{
    public static O Substitute<I,O>(I obj, Func<I,O> fn, O nullValue=default(O))
    {
        if (obj == null)
            return nullValue;
        else
            return fn(obj);
    }
}

Pierwszym argumentem jest testowany obiekt. Druga to funkcja. A trzecia to wartość zerowa. Więc w twoim przypadku:

IsNull.Substitute(Session["key"],s=>s.ToString(),"none");

Jest to również bardzo przydatne w przypadku typów dopuszczających wartość null. Na przykład:

decimal? v;
...
IsNull.Substitute(v,v.Value,0);
....
Tomaz Stih
źródło