Przykłady przeciążenia operatora, które mają sens [zamknięty]

12

Podczas nauki języka C # okazało się, że C # obsługuje przeciążanie operatora. Mam problem z dobrym przykładem, który:

  1. Ma sens (np. Dodanie klasy o nazwie owca i krowa)
  2. Nie jest przykładem łączenia dwóch łańcuchów

Przykłady z biblioteki klas podstawowych są mile widziane.

Paweł Sołtysiak
źródło
10
Proszę zdefiniować „sens”! Poważnie, gorzkie i pełne pasji debaty na ten temat pokazują, że istnieje ogromna różnica zdań na ten temat. Wiele organów odrzuca przeciążonych operatorów, ponieważ można ich zmuszać do robienia całkowicie nieoczekiwanych rzeczy. Inni odpowiadają, że nazwy metod można również wybrać jako całkowicie nieintuicyjne, ale nie jest to powód do odrzucania nazwanych bloków kodu! Prawie na pewno nie dostaniesz żadnych przykładów, które są ogólnie uważane za rozsądne. Przykłady, które wydają się rozsądne, aby was - być może.
Kilian Foth
Całkowicie zgadzam się z @KilianFoth. Ostatecznie program, który się kompiluje, ma sens w kompilacji. Ale jeśli przeciążenie ==robi mnożenie, ma to dla mnie sens, ale może nie mieć sensu dla innych! Czy to pytanie dotyczy zasadności języków programowania w obiektach, czy mówimy o „kodowaniu najlepszych praktyk”?
Dipan Mehta

Odpowiedzi:

27

Oczywistym przykładem odpowiedniego przeciążenia operatora są dowolne klasy, które zachowują się tak samo, jak działają liczby. Tak więc klasy BigInt (jak sugeruje Jalayn ), liczby zespolone lub klasy macierzy (jak sugeruje Superbest ) wszystkie mają te same operacje, które zwykłe liczby tak dobrze odwzorowują na operatory matematyczne, podczas gdy operacje czasowe (jak sugeruje svick ) ładnie odwzorowują podzbiór tych operacji.

Nieco bardziej abstrakcyjnie, operatory mogą być używane podczas wykonywania operacji typu set , więc operator+może być związkiem , operator-może być uzupełnieniem itp. To jednak zaczyna rozciągać paradygmat, szczególnie jeśli używasz operatora dodawania lub mnożenia dla operacji, która nie jest t przemienny , jak można się spodziewać.

Sam C # ma doskonały przykład przeciążenia operatora nienumerycznego . Wykorzystuje +=i -=do dodawania i odejmowania delegatów , tj. Rejestrowania i wyrejestrowywania ich. Działa to dobrze, ponieważ operatory +=i -=działają tak, jak byś tego oczekiwał, a to skutkuje znacznie bardziej zwięzłym kodem.

Dla purysty jednym z problemów z +operatorem łańcucha jest to, że nie jest on przemienny. "a"+"b"to nie to samo co "b"+"a". Rozumiemy ten wyjątek dla ciągów, ponieważ jest tak powszechny, ale jak możemy stwierdzić, czy użycie operator+na innych typach będzie przemienne, czy nie? Większość ludzi zakłada, że ​​tak jest, chyba że obiekt jest podobny do łańcucha , ale tak naprawdę nigdy nie wiadomo, co ludzie przyjmą.

Podobnie jak w przypadku łańcuchów, wątki matryc są również dość dobrze znane. Oczywiste jest, że Matrix operator* (double, Matrix)jest to mnożenie skalarne, podczas gdy na przykład Matrix operator* (Matrix, Matrix)byłoby mnożeniem macierzowym (tj. Macierzą mnożenia iloczynu iloczynowego).

Podobnie użycie operatorów z delegatami jest tak bardzo dalekie od matematyki, że jest mało prawdopodobne, aby popełniono te błędy.

Nawiasem mówiąc, na konferencji ACCU w 2011 r. Roger Orr i Steve Love przedstawili sesję „ Niektóre przedmioty są bardziej równe niż inne” - spojrzenie na wiele znaczeń równości, wartości i tożsamości . Ich slajdy można pobrać , podobnie jak dodatek Richarda Harrisa o równości zmiennoprzecinkowej . Podsumowanie: Należy być bardzo ostrożnym z operator==tu być smoki!

Przeciążenie operatora jest bardzo potężną techniką semantyczną, ale jest łatwe do nadmiernego użycia. Idealnie powinieneś go używać tylko w sytuacjach, gdy z kontekstu jasno wynika, jaki jest efekt przeciążenia operatora. Pod wieloma względami a.union(b)jest jaśniejsze niż a+bi a*bjest o wiele bardziej niejasne niż a.cartesianProduct(b), zwłaszcza że wynik działania kartezjańskiego byłby SetLike<Tuple<T,T>>raczej wynikiem niż SetLike<T>.

Prawdziwe problemy z przeciążeniem operatora pojawiają się, gdy programista zakłada, że ​​klasa będzie zachowywać się w jeden sposób, ale w rzeczywistości zachowuje się w inny sposób. Sugeruję, że tego rodzaju starcie semantyczne należy unikać.

Mark Booth
źródło
1
Mówisz, że operatory na macierzach odwzorowują naprawdę dobrze, ale mnożenie macierzy też nie jest przemienne. Również operatorzy na delegatach są jeszcze silniejsi. Możesz zrobić d1 + d2dla dowolnych dwóch delegatów tego samego typu.
sick
1
@ Mark: „Produkt kropkowy” jest zdefiniowany tylko w wektorach; pomnożenie dwóch macierzy nazywa się po prostu „mnożeniem macierzy”. To rozróżnienie jest czymś więcej niż tylko semantycznym: iloczyn kropkowy zwraca skalar, podczas gdy mnożenie macierzy zwraca macierz (i, nawiasem mówiąc, jest nieprzemienny) .
BlueRaja - Danny Pflughoeft
26

Dziwię się, że nikt nie wspomniał o jednym z bardziej interesujących przypadków w BCL: DateTimei TimeSpan. Możesz:

  • dodaj lub odejmij dwa TimeSpans, aby uzyskać innyTimeSpan
  • użyj unary minus na a, TimeSpanaby uzyskać negacjęTimeSpan
  • odejmij dwa DateTimes, aby uzyskaćTimeSpan
  • dodaj lub odejmij TimeSpanod, DateTimeaby uzyskać innyDateTime

Inny zestaw operatorów, które mogłyby spowodować poczucie na wiele typów są <, >, <=, >=. Na przykład w BCL Versionimplementuje je.

svick
źródło
Bardzo prawdziwy przykład zamiast teorii pedantycznych!
SIslam,
7

Pierwszym przykładem, który przychodzi mi na myśl, jest implementacja BigInteger , która pozwala na pracę z dużymi liczbami całkowitymi ze znakiem . Sprawdź link MSDN, aby zobaczyć, ilu operatorów zostało przeciążonych (to znaczy, istnieje duża lista, a ja nie sprawdziłem, czy wszyscy operatorzy zostali przeciążeni, ale na pewno tak jest)

Ponadto, ponieważ robię również Javę, a Java nie pozwala na przeciążanie operatorów, pisanie jest niesamowicie słodsze

BigInteger bi = new BigInteger(0);
bi += 10;

Następnie w Javie:

BigDecimal bd = new BigDecimal(0);
bd = bd.add(new BigDecimal(10));
Jalayn
źródło
5

Cieszę się, że to widziałem, ponieważ wygłupiałem się z Irony i WIELKIE zastosowanie przeciążenia operatora. Oto próbka tego, co może zrobić.

Irony jest więc „zestawem implementacyjnym języka .NET” i generatorem analizatora składni (generującym analizator składni LALR). Zamiast nauczyć się nowej składni / języka, takiego jak generatory parsera, takie jak yacc / lex, piszesz gramatykę w języku C # z przeciążeniem operatora. Oto prosta gramatyka BNF

// BNF 
Expr := Term | BinExpr
Term := number | ParExpr
ParExpr := "(" + Expr + ")"
BinExpr := number + BinOp + number
BinOp := "+" | "-" | "*" | "/"
number := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Jest to więc prosta gramatyka (przepraszam, jeśli występują niespójności, ponieważ uczę się BNF i gramatyki). Teraz spójrzmy na C #:

  var Expr = new NonTerminal("Expr");
  var Term = new NonTerminal("Term");
  var BinExpr = new NonTerminal("BinExpr");
  var ParExpr = new NonTerminal("ParExpr");
  var BinOp = new NonTerminal("BinOp");
  var Statement = new NonTerminal("Statement");
  var ProgramLine = new NonTerminal("ProgramLine");
  var Program = new NonTerminal("Program", typeof(StatementListNode));
  // BNF Rules - Overloading
  Expr.Rule = Term | BinExpr;
  Term.Rule = number | ParExpr;
  ParExpr.Rule = "(" + Expr + ")";
  BinExpr.Rule = Expr + BinOp + Expr;
  BinOp.Rule = ToTerm("+") | "-" | "*" | "/" | "**";

Jak widać, przy przeciążeniu operatora pisanie gramatyki w języku C # jest prawie dokładnie pisaniem gramatyki w języku BNF. Dla mnie to nie tylko ma sens, ale jest świetnym zastosowaniem przeciążenia operatora.

Jetti
źródło
3

Kluczowym przykładem jest operator == / operator! =.

Jeśli chcesz łatwo porównać dwa obiekty według wartości danych zamiast przez odniesienie, będziesz chciał przeciążać .Equals (i.GetHashCode!), A dla zachowania spójności możesz również chcieć wykonać operatory! = I ==.

Nigdy nie widziałem żadnych dzikich przeciążeń innych operatorów w C # (wyobrażam sobie, że istnieją skrajne przypadki, w których może to być przydatne).

Ed James
źródło
1

Ten przykład z MSDN pokazuje, jak zaimplementować liczby zespolone i pozwolić, aby używały normalnego operatora +.

Kolejny przykład pokazuje, jak to zrobić w celu dodania macierzy, a także wyjaśnia, jak nie używać go do dodawania samochodu do garażu (przeczytaj link).

Superbest
źródło
0

Dobre wykorzystanie przeciążenia może być rzadkie, ale tak się dzieje.

przeciążanie operatora == i operatora! = pokaż dwie szkoły myślenia: te za powiedzenie, że to ułatwia, a te przeciwko temu, że uniemożliwiają porównywanie adresów (tj. czy wskazuję dokładnie to samo miejsce w pamięci, a nie tylko kopię tego samego obiekt).

Uważam, że przeciążenia operatora rzutowania są przydatne w określonych sytuacjach. Na przykład musiałem serializować / deserializować w formacie XML wartość logiczną reprezentowaną przez 0 lub 1. Właściwy (domyślny lub jawny, zapominam) operator rzutowania z wartości logicznej na int i odwrotnie.

MPelletier
źródło
4
Nie uniemożliwia to porównywania adresów: Nadal możesz używać object.ReferenceEquals().
dan04
@ dan04 Bardzo, bardzo dobrze wiedzieć!
MPelletier
Innym sposobem porównywania adresów jest wymuszenie użycia obiektu ==przez rzutowanie: (object)foo == (object)barzawsze porównuje odniesienia. Ale wolałbym ReferenceEquals(), jak wspomina @ dan04, ponieważ jest wyraźniejsze, co robi.
svick
0

Nie należą do kategorii rzeczy, które ludzie zwykle myślą o przeciążeniu operatora, ale myślę, że jednym z najważniejszych operatorów, którzy mogą przeciążać, jest operator konwersji .

Operatory konwersji są szczególnie przydatne w przypadku typów wartości, które mogą „oddzielić cukier” od typu liczbowego lub mogą działać jak typ liczbowy w niektórych kontekstach. Na przykład, można zdefiniować specjalny Idrodzaj, który reprezentuje pewien identyfikator, i można zapewnić niejawna konwersja do inttak, że można przekazać Iddo metody, która trwa int, ale explict konwersji od intdo Idtak nikt nie może przekazać intdo metoda, która wymaga Idnajpierw bez rzutowania.

Na przykład poza C # język Python zawiera wiele specjalnych zachowań, które są implementowane jako przeciążalne operatory. Należą do nich inoperator do testowania członkostwa, ()operator do wywoływania obiektu tak, jakby to była funkcja, oraz lenoperator do określania długości lub wielkości obiektu.

A potem masz języki takie jak Haskell, Scala i wiele innych języków funkcjonalnych, w których nazwy +są zwykłymi funkcjami, a nie operatorami w ogóle (i istnieje obsługa języków dla używania funkcji w pozycji infiksowej).

Daniel Pryden
źródło
0

Struktura punktów w przestrzeni nazw System.Drawing wykorzystuje przeciążenie, aby porównać dwie różne lokalizacje za pomocą przeciążenia operatora.

 Point locationA = new Point( 50, 50 );
 Point locationB = new Point( 50, 50 );

 if ( locationA == locationB )
    Console.WriteLine( "Their locations are the same" );
 else
    Console.WriteLine( "Their locations  are different" );

Jak widać, o wiele łatwiej jest porównać współrzędne X i Y dwóch lokalizacji za pomocą przeciążenia.

Karthik Sreenivasan
źródło
0

Jeśli znasz wektor matematyczny, możesz zobaczyć zastosowanie w przeciążeniu +operatora. Możesz dodać wektor za a=[1,3]pomocą b=[2,-1]i uzyskać c=[3,2].

Przeciążenie równości (==) może być również przydatne (chociaż prawdopodobnie lepiej jest zaimplementować equals()metodę). Aby kontynuować przykłady wektorowe:

v1=[1,3]
v2=[1,3]
v1==v2 // True
MartinHaTh
źródło
-2

Wyobraź sobie kawałek kodu do rysowania na formularzu

{
  Point p = textBox1.Location;
  Size dp = textBox1.Size;

  // Here the + operator has been overloaded by the CLR
  p += dp;  // Now p points to the lower right corner of the textbox.
  ..
}

Innym częstym przykładem jest użycie struktury do przechowywania informacji o pozycji w postaci wektora.

public struct Pos
{
    public double x, y, z;
    public double Distance { get { return Math.Sqrt(x * x + y * y + z * z); } }
    public static Pos operator +(Pos A, Pos B)
    {
        return new Pos() { x = A.x + B.x, y = A.y + B.y, z = A.z + B.z };
    }
    public static Pos operator -(Pos A, Pos B)
    {
        return new Pos() { x = A.x - B.x, y = A.y - B.y, z = A.z - B.z };
    }
}

do wykorzystania później jako

{
    Pos A = new Pos() { x = 4, y = -1, z = 0.5 };
    Pos B = new Pos() { x = 8, y = 2, z = 1.5 };

    double x = (B - A).Distance;
}
ja72
źródło
4
Dodawania wektorów, a nie pozycje: \ Jest to dobry przykład na to, kiedy operator+powinno nie być przeciążone (można wdrożyć punkt pod względem wektora, ale nie powinien być w stanie dodać dwa punkty)
BlueRaja - Danny Pflughoeft
@ BlueRaja-DannyPflughoeft: Dodawanie pozycji w celu uzyskania innej pozycji nie ma sensu, ale odejmuje je (w celu uzyskania wektora), podobnie jak ich uśrednianie . Można obliczyć średnią p1, p2, p3 i p4 przez p1+((p2-p1)+(p3-p1)+(p4-p1))/4, ale wydaje się to nieco niezręczne.
supercat
1
W geometrii afinicznej można wykonywać algebrę z punktami i liniami, takimi jak dodawanie, skalowanie itp. Implementacja wymaga jednak jednorodnych współrzędnych, które i tak są zwykle używane w grafice 3D. Dodanie dwóch punktów powoduje ich średnią.
ja72