Unikalne sposoby korzystania z operatora Null Coalescing [zamknięte]

164

Wiem, że standardowym sposobem używania operatora koalescencji Null w C # jest ustawienie wartości domyślnych.

string nobody = null;
string somebody = "Bob Saget";
string anybody = "";

anybody = nobody   ?? "Mr. T"; // returns Mr. T
anybody = somebody ?? "Mr. T"; // returns "Bob Saget"

Ale do czego jeszcze można ??wykorzystać? Nie wydaje się tak użyteczny jak operator trójskładnikowy, poza tym, że jest bardziej zwięzły i łatwiejszy do odczytania niż:

nobody = null;
anybody = nobody == null ? "Bob Saget" : nobody; // returns Bob Saget

Więc biorąc pod uwagę, że mniej nawet wie o zerowym operatorze koalescencji ...

  • Czy używałeś ??czegoś innego?

  • Jest to ??konieczne lub po prostu użyj operatora trójskładnikowego (który większość zna)

Armstrongest
źródło

Odpowiedzi:

216

Po pierwsze, łańcuch jest znacznie łatwiejszy niż standardowy trójskładnik:

string anybody = parm1 ?? localDefault ?? globalDefault;

vs.

string anyboby = (parm1 != null) ? parm1 
               : ((localDefault != null) ? localDefault 
               : globalDefault);

Działa również dobrze, jeśli obiekt z możliwością zerowania nie jest zmienną:

string anybody = Parameters["Name"] 
              ?? Settings["Name"] 
              ?? GlobalSetting["Name"];

vs.

string anybody = (Parameters["Name"] != null ? Parameters["Name"] 
                 : (Settings["Name"] != null) ? Settings["Name"]
                 :  GlobalSetting["Name"];
James Curran
źródło
12
Łańcuch to duży plus dla operatora, usuwa kilka zbędnych IF
chakrit
1
Po prostu użyłem go dzisiaj, aby zastąpić prosty blok IF, który napisałem, zanim dowiedziałem się o operatorze koalescencyjnym potrójnym lub zerowym. Gałęzie prawda i fałsz oryginalnej instrukcji IF nazywają tę samą metodę, zastępując jeden z jej argumentów inną wartością, jeśli pewne dane wejściowe mają wartość NULL. Z operatorem koalescencji zerowej to jedno wywołanie. Jest to naprawdę potężne, gdy masz metodę, która wymaga dwóch lub więcej takich podstawień!
David A. Gray
177

Użyłem go jako linijki z leniwym ładowaniem:

public MyClass LazyProp
{
    get { return lazyField ?? (lazyField = new MyClass()); }
}

Czytelny? Zdecyduj sam.

Cristian Libardo
źródło
6
Hmmm, znalazłeś kontrprzykład „dlaczego ktoś miałby chcieć użyć tego jako zaciemnionego IF”… który jest dla mnie bardzo czytelny.
Godeke
6
Być może czegoś brakuje (głównie używam Javy), ale czy nie ma tam warunków wyścigu?
Justin K
9
@Justin K - stan wyścigu występuje tylko wtedy, gdy wiele wątków uzyskuje dostęp do właściwości LazyProp tego samego obiektu. Można go łatwo naprawić za pomocą zamka, jeśli wymagane jest zabezpieczenie wątku każdego wystąpienia. Oczywiście w tym przykładzie nie jest to wymagane.
Jeffrey L Whitledge
5
@Jeffrey: Gdyby było jasne, nie zadałbym tego pytania. :) Kiedy zobaczyłem ten przykład, od razu pomyślałem o pojedynczym składniku, a ponieważ zdarzyło mi się robić większość swojego kodu w środowisku wielowątkowym ... Ale tak, jeśli założymy, że kod jest poprawny, nic więcej jest niepotrzebne.
Justin K
7
Nie musi to być singleton, żeby mieć wyścig. Tylko współdzielone wystąpienie klasy, która zawiera LazyProp, i wiele wątków uzyskujących dostęp do LazyProp. Lazy <T> jest lepszym sposobem na zrobienie tego rodzaju rzeczy i jest domyślnie bezpieczny wątkowo (możesz wybrać modyfikację bezpieczeństwa wątków Lazy <T>).
Niall Connaughton,
52

Uważam, że jest przydatny na dwa „nieco dziwne” sposoby:

  • Jako alternatywa dla posiadania outparametru podczas pisania TryParseprocedur (tj. Zwraca wartość null, jeśli parsowanie nie powiedzie się)
  • Jako reprezentacja „nie wiem” dla porównań

Ten ostatni potrzebuje trochę więcej informacji. Zwykle, gdy tworzysz porównanie z wieloma elementami, musisz sprawdzić, czy pierwsza część porównania (np. Wiek) daje ostateczną odpowiedź, a następnie następna (np. Imię) tylko wtedy, gdy pierwsza część nie pomogła. Używanie operatora łączenia wartości null oznacza, że ​​możesz pisać całkiem proste porównania (czy to w celu uporządkowania, czy równości). Na przykład, używając kilku klas pomocniczych w MiscUtil :

public int Compare(Person p1, Person p2)
{
    return PartialComparer.Compare(p1.Age, p2.Age)
        ?? PartialComparer.Compare(p1.Name, p2.Name)
        ?? PartialComparer.Compare(p1.Salary, p2.Salary)
        ?? 0;
}

Wprawdzie mam teraz ProjectionComparer w MiscUtil, wraz z kilkoma rozszerzeniami, które sprawiają, że tego rodzaju rzeczy są jeszcze łatwiejsze - ale nadal jest fajne.

To samo można zrobić, aby sprawdzić równość odwołań (lub nieważność) na początku implementowania Equals.

Jon Skeet
źródło
Podoba mi się to, co zrobiłeś z PartialComparer, ale szukałem przypadków, w których muszę zachować wartościowane zmienne wyrażeń. Nie jestem zorientowany w lambdach i rozszerzeniach, więc czy możesz zobaczyć, czy następujący wzór jest zgodny z podobnym wzorcem (tj. Czy to działa)? stackoverflow.com/questions/1234263/#1241780
maxwellb
33

Kolejną zaletą jest to, że operator trójskładnikowy wymaga podwójnej oceny lub zmiennej tymczasowej.

Rozważmy to na przykład:

string result = MyMethod() ?? "default value";

podczas gdy z operatorem trójskładnikowym pozostaje:

string result = (MyMethod () != null ? MyMethod () : "default value");

która dwukrotnie wywołuje MyMethod lub:

string methodResult = MyMethod ();
string result = (methodResult != null ? methodResult : "default value");

Tak czy inaczej, operator koalescencji zerowej jest czystszy i, jak sądzę, bardziej wydajny.


źródło
1
+1. Jest to jeden z głównych powodów, dla których podoba mi się operator koalescencji zerowej. Jest to szczególnie przydatne, gdy dzwonienie MyMethod()ma jakiekolwiek skutki uboczne.
CVn
Jeśli MyMethod()nie ma żadnych efektów poza zwracaniem wartości, kompilator wie, aby nie wywoływać jej dwukrotnie, więc w większości przypadków nie musisz się martwić o wydajność.
TinyTimZamboni
Zachowuje również czytelność IMHO, gdy MyMethod()jest to połączona sekwencja kropkowanych obiektów. Np .:myObject.getThing().getSecondThing().getThirdThing()
xdhmoore
@TinyTimZamboni, czy masz odniesienie do tego zachowania kompilatora?
Kuba Wyrostek
@KubaWyrostek Nie mam wiedzy na temat specyfiki działania kompilatora C #, ale mam pewne doświadczenie z teorią kompilatora statycznego z llvm. Jest kilka podejść, które kompilator może zastosować, aby zoptymalizować takie wywołanie. Globalna numeracja wartości zauważy, że dwa wywołania MyMethodsą identyczne w tym kontekście, zakładając, że MyMethodjest to funkcja Pure. Inną opcją jest Automatyczne zapamiętywanie lub po prostu zamknięcie funkcji w pamięci podręcznej. Z drugiej strony: en.wikipedia.org/wiki/Global_value_numbering
TinyTimZamboni
23

Inną rzeczą do rozważenia jest to, że operator koalescencji nie wywołuje metody get właściwości dwukrotnie, jak robi to trójskładnik.

Są więc scenariusze, w których nie powinieneś używać trójskładnika, na przykład:

public class A
{
    var count = 0;
    private int? _prop = null;
    public int? Prop
    {
        get 
        {
            ++count;
            return _prop
        }
        set
        {
            _prop = value;
        }
    }
}

Jeśli użyjesz:

var a = new A();
var b = a.Prop == null ? 0 : a.Prop;

getter zostanie wywołany dwukrotnie, a countzmienna będzie równa 2, a jeśli użyjesz:

var b = a.Prop ?? 0

countzmienna będzie równa 1, jak powinno.

Fabio Lima
źródło
4
To zasługuje na więcej głosów. Czytałem sooo wiele razy, że ??jest równoważny do ?:.
Kuba Wyrostek
1
Ważny punkt o dwukrotnym wywołaniu metody pobierającej. Ale ten przykład uznałbym za zły wzorzec projektowy, w którym taki mylnie nazwany getter faktycznie wprowadza zmiany w obiekcie.
Linas
15

Największą zaletą, jaką widzę dla ??operatora, jest to, że można łatwo przekonwertować typy wartości dopuszczające wartość null na typy nieprzekraczające wartości null:

int? test = null;
var result = test ?? 0; // result is int, not int?

Często używam tego w zapytaniach Linq:

Dictionary<int, int?> PurchaseQuantities;
// PurchaseQuantities populated via ASP .NET MVC form.
var totalPurchased = PurchaseQuantities.Sum(kvp => kvp.Value ?? 0);
// totalPurchased is int, not int?
Ryan
źródło
Może się trochę spóźnię, ale ten drugi przykład poda, jeśli kvp == null. I faktycznie Nullable<T>ma GetValueOrDefaultmetodę, której normalnie używam.
CompuChip
6
KeyValuePair jest typem wartości w środowisku .NET, więc dostęp do żadnej z jego właściwości nigdy nie spowodowałby zerowego wyjątku odwołania. msdn.microsoft.com/en-us/library/5tbh8a42(v=vs.110).aspx
Ryan
9

Użyłem ?? w mojej implementacji IDataErrorInfo:

public string Error
{
    get
    {
        return this["Name"] ?? this["Address"] ?? this["Phone"];
    }
}

public string this[string columnName]
{
    get { ... }
}

Jeśli jakaś właściwość jest w stanie „błędu”, otrzymuję ten błąd, w przeciwnym razie otrzymuję wartość null. Działa naprawdę dobrze.

Matt Hamilton
źródło
Ciekawy. Używasz „this” jako właściwości. Nigdy tego nie robiłem.
Armstrongest
Tak, to część sposobu działania IDataErrorInfo. Generalnie ta składnia jest przydatna tylko w klasach kolekcji.
Matt Hamilton,
4
Jesteś przechowywania komunikatów o błędach w this["Name"], this["Address"]itp ??
Andrew
7

Możesz użyć operatora koalescencji null, aby uczynić go nieco czystszym w obsłudze przypadku, w którym opcjonalny parametr nie jest ustawiony:

public void Method(Arg arg = null)
{
    arg = arg ?? Arg.Default;
    ...
Niall Connaughton
źródło
Czy nie byłoby wspaniale, gdyby tę linię można zapisać jako arg ?= Arg.Default?
Jesse de Wit
6

Lubię używać operatora koalescencji zerowej do leniwego ładowania niektórych właściwości.

Bardzo prosty (i wymyślony) przykład, aby zilustrować mój punkt widzenia:

public class StackOverflow
{
    private IEnumerable<string> _definitions;
    public IEnumerable<string> Definitions
    {
        get
        {
            return _definitions ?? (
                _definitions = new List<string>
                {
                    "definition 1",
                    "definition 2",
                    "definition 3"
                }
            );
        }
    } 
}
mlnyc
źródło
Resharper faktycznie zasugeruje to jako refaktor dla „tradycyjnego” leniwego obciążenia.
arichards
5

Jest ?? konieczne lub po prostu użyj operatora trójskładnikowego (który większość zna)

Właściwie, z mojego doświadczenia wynika, że ​​zbyt mało osób zna operator trójskładnikowy (a dokładniej operator warunkowy ; ?:jest „trójskładnikowy” w tym samym sensie, co ||binarny lub +jednoargumentowy lub binarny; jednak zdarza się, że jest to tylko operator trójskładnikowy w wielu językach), więc przynajmniej w tej ograniczonej próbce twoja instrukcja zawodzi.

Ponadto, jak wspomniano wcześniej, istnieje jedna poważna sytuacja, w której operator koalescencji zerowej jest bardzo przydatny, a jest to ilekroć oceniane wyrażenie ma jakiekolwiek skutki uboczne. W takim przypadku nie można użyć operatora warunkowego bez (a) wprowadzenia zmiennej tymczasowej lub (b) zmiany rzeczywistej logiki aplikacji. (b) jest wyraźnie nieodpowiednie w żadnych okolicznościach i chociaż jest to osobiste preferencje, nie lubię zaśmiecać zakresu deklaracji wieloma zewnętrznymi, nawet krótkotrwałymi, zmiennymi, więc (a) też jest w tym szczególny scenariusz.

Oczywiście, jeśli musisz przeprowadzić wielokrotne sprawdzenie wyniku, ifprawdopodobnie narzędziem do tego zadania jest operator warunkowy lub zestaw bloków. Ale dla prostego „jeśli to jest null, użyj tego, w przeciwnym razie użyj tego”, operator koalescencji zerowej ??jest doskonały.

CVn
źródło
Bardzo późny komentarz ode mnie - ale miło, że ktoś opisuje, że operator trójskładnikowy to operator z trzema argumentami (z których jest teraz więcej niż jeden w C #).
Steve Kidd
5

Jedną z rzeczy, które ostatnio często robiłem, jest koalescencja zerowa do tworzenia kopii zapasowych as. Na przykład:

object boxed = 4;
int i = (boxed as int?) ?? 99;

Console.WriteLine(i); // Prints 4

Jest to również przydatne do tworzenia kopii zapasowych długich łańcuchów, z ?.których każdy może zawieść

int result = MyObj?.Prop?.Foo?.Val ?? 4;
string other = (MyObj?.Prop?.Foo?.Name as string)?.ToLower() ?? "not there";
Niebieski0500
źródło
4

Jedynym problemem jest to, że operator łączenia zerowego nie wykrywa pustych ciągów.


to znaczy

string result1 = string.empty ?? "dead code!";

string result2 = null ?? "coalesced!";

WYNIK:

result1 = ""

result2 = coalesced!

Obecnie pracuję nad zastąpieniem ?? operator, aby obejść ten problem. Z pewnością przydałoby się mieć to wbudowane w Framework.

Myśli?

Kevin Panko
źródło
Możesz to zrobić za pomocą metod rozszerzenia, ale zgadzam się, byłby to fajny dodatek do kodu i bardzo przydatny w kontekście sieciowym.
Armstrongest
Tak, to jest częsty scenariusz ... jest nawet specjalna metoda String.IsNullOrEmpty (string) ...
Max Galkin
12
„operator łączenia zerowego nie wykrywa pustych łańcuchów”. Cóż, jest to operator koalescencji null , a nie operator koalescencji nullOrEmpty. Osobiście gardzę mieszaniem wartości zerowych i pustych w językach, w których są one rozróżniane, co sprawia, że ​​łączenie się z rzeczami, które nie jest zbyt denerwujące. I jestem trochę obsesyjno-kompulsywny, więc denerwuje mnie, gdy języki / implementacje i tak nie rozróżniają tych dwóch, nawet jeśli rozumiem powód (jak w [większości implementacji?] SQL).
JAB,
3
??nie może być przeciążony: msdn.microsoft.com/en-us/library/8edha89s(v=vs.100).aspx - byłoby jednak świetnie, gdyby można było go przeciążać. Używam kombinacji: s1.Nullify() ?? s2.Nullify()gdzie string Nullify(this s)zwraca a nullw przypadkach, gdy ciąg jest pusty.
Kit
Jedyny problem? Po prostu stwierdziłem, że chcę ?? = i znalazłem ten wątek, sprawdzając, czy jest na to sposób. (Sytuacja: pierwszy przebieg załadował przypadki wyjątków, teraz chcę wrócić i załadować domyślne wartości do wszystkiego, co jeszcze nie zostało załadowane.)
Loren Pechtel
3

Jest ?? konieczne lub po prostu użyj operatora trójskładnikowego (który większość zna)

Powinieneś używać tego, co najlepiej wyraża twoje zamiary. Ponieważ nie jest operatorem COALESCE null, używać go .

Z drugiej strony, ponieważ jest tak wyspecjalizowany, nie sądzę, aby miał inne zastosowania. Wolałbym odpowiednie przeciążenie ||operatora, tak jak robią to inne języki. Byłoby to bardziej oszczędne w projekcie języka. Ale cóż…

Konrad Rudolph
źródło
3

Chłodny! Policz mnie jako kogoś, kto nie wiedział o zerowym operatorze koalescencji - to całkiem fajna rzecz.

Uważam, że jest dużo łatwiejszy do odczytania niż operator trójskładnikowy.

Pierwszym miejscem, które przychodzi mi na myśl, gdzie mógłbym go użyć, jest przechowywanie wszystkich moich domyślnych parametrów w jednym miejscu.

public void someMethod( object parm2, ArrayList parm3 )
{ 
  someMethod( null, parm2, parm3 );
}
public void someMethod( string parm1, ArrayList parm3 )
{
  someMethod( parm1, null, parm3 );
}
public void someMethod( string parm1, object parm2, )
{
  someMethod( parm1, parm2, null );
}
public void someMethod( string parm1 )
{
  someMethod( parm1, null, null );
}
public void someMethod( object parm2 )
{
  someMethod( null, parm2, null );
}
public void someMethod( ArrayList parm3 )
{
  someMethod( null, null, parm3 );
}
public void someMethod( string parm1, object parm2, ArrayList parm3 )
{
  // Set your default parameters here rather than scattered through the above function overloads
  parm1 = parm1 ?? "Default User Name";
  parm2 = parm2 ?? GetCurrentUserObj();
  parm3 = parm3 ?? DefaultCustomerList;

  // Do the rest of the stuff here
}
HanClinto
źródło
2

Trochę dziwny przypadek użycia, ale miałem metodę, w której IDisposableobiekt jest potencjalnie przekazywany jako argument (i dlatego usuwany przez rodzica), ale może być również pusty (i dlatego powinien zostać utworzony i usunięty w metodzie lokalnej)

Aby go użyć, kod wyglądał jak

Channel channel;
Authentication authentication;

if (entities == null)
{
    using (entities = Entities.GetEntities())
    {
        channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
        [...]
    }
}
else
{
    channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
    [...]
}

Ale z zerowym połączeniem staje się znacznie schludniejszy

using (entities ?? Entities.GetEntities())
{
    channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
    [...]
}
PaulG
źródło
0

Użyłem w ten sposób:

for (int i = 0; i < result.Count; i++)
            {
                object[] atom = result[i];

                atom[3] = atom[3] ?? 0;
                atom[4] = atom[4] != null ? "Test" : string.Empty;
                atom[5] = atom[5] ?? "";
                atom[6] = atom[6] ?? "";
                atom[7] = atom[7] ?? "";
                atom[8] = atom[8] ?? "";
                atom[9] = atom[9] ?? "";
                atom[10] = atom[10] ?? "";
                atom[12] = atom[12] ?? false; 
            }
Muhammad Awais
źródło