Dobre praktyki przeciążania operatorów
Dbaj o intuicyjne działanie przeciążonych operatorów.
Dobre praktyki przeciążania operatorów to bezpłatna lekcja C# Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C# Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C# Academy zawiera 4 lekcji w sumie.
Dobre praktyki przeciążania operatorów
Przeciążanie operatorów to narzędzie o dużych możliwościach. Właściwie użyte sprawia, że typy wartości są intuicyjne; użyte niewłaściwie czyni kod trudnym do zrozumienia. Ta lekcja omawia zasady, dzięki którym przeciążone operatory pozostają przewidywalne.
Przeciążaj tylko wtedy, gdy znaczenie jest oczywiste
Operator należy przeciążać tylko wtedy, gdy jego znaczenie dla danego typu jest jednoznaczne. + dla wektora wyraźnie oznacza dodawanie składowych; + dla typu Order jedynie wprowadzałby czytelników w błąd.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
// + means adding components: obvious and expected
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Vector2(1, 2) + new Vector2(3, 4));
}
}Zachowaj oczekiwania matematyczne
Operatory powinny być zgodne z intuicją: a + b powinno być równe b + a, gdy działanie jest przemienne, a + nie powinien potajemnie odejmować. Takie niespodzianki powodują trudne do wykrycia błędy.
using System;
public struct Money
{
public decimal Amount;
public Money(decimal a) { Amount = a; }
public static Money operator +(Money a, Money b) => new Money(a.Amount + b.Amount);
public override string ToString() => "$" + Amount;
}
public class Program
{
public static void Main()
{
var x = new Money(3);
var y = new Money(5);
Console.WriteLine((x + y).Amount == (y + x).Amount); // commutative
}
}Zachowaj symetrię i pary operatorów
Język łączy niektóre operatory w pary, ale należy również zachować logiczną symetrię: jeśli dodano +, warto rozważyć także -; jeśli dodano <, należy udostępnić również > oraz warianty z operatorem równości. Niepełne zestawy operatorów frustrują użytkowników.
using System;
public struct Length
{
public int Mm;
public Length(int mm) { Mm = mm; }
public static Length operator +(Length a, Length b) => new Length(a.Mm + b.Mm);
public static Length operator -(Length a, Length b) => new Length(a.Mm - b.Mm);
public override string ToString() => Mm + "mm";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Length(50) + new Length(20));
Console.WriteLine(new Length(50) - new Length(20));
}
}Nie modyfikuj operandów
Operatory powinny być czyste: obliczać i zwracać nową wartość, pozostawiając operandy bez zmian. Modyfikowanie operandu wewnątrz + łamie wszelkie oczekiwania czytelnika.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y); // returns new, mutates nothing
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var a = new Vector2(1, 1);
var unused = a + new Vector2(9, 9);
Console.WriteLine("a is still " + a);
}
}Zachowaj zgodność == z Equals
Powtarzana dobra praktyka: po przeciążeniu == należy przesłonić Equals i GetHashCode, aby wszystkie ścieżki porównywania dawały zgodny wynik. Niespójność w tym miejscu jest jednym z najczęstszych błędów związanych z operatorami.
using System;
public struct Point
{
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
public override bool Equals(object obj) => obj is Point p && p.X == X && p.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
public static bool operator ==(Point a, Point b) => a.Equals(b);
public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Point(1, 1) == new Point(1, 1));
}
}Udostępniaj nazwane alternatywy
Niektóre języki i narzędzia do analizy kodu nie mogą używać operatorów. Udostępnienie nazwanej metody, takiej jak Add, obok + poprawia współdziałanie i ułatwia odnajdywanie funkcji.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public Vector2 Add(Vector2 other) => new Vector2(X + other.X, Y + other.Y);
public static Vector2 operator +(Vector2 a, Vector2 b) => a.Add(b);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var v = new Vector2(1, 2);
Console.WriteLine(v.Add(new Vector2(3, 4)));
Console.WriteLine(v + new Vector2(3, 4));
}
}Preferuj operatory dla typów przypominających wartości
Operatory są naturalne w przypadku niezmiennych typów przypominających wartości (struktur lub klas podobnych do rekordów). W przypadku typów zmiennych, w których istotna jest tożsamość, nazwane metody zwykle lepiej komunikują intencję.
using System;
public readonly struct Rational
{
public readonly int Num, Den;
public Rational(int n, int d) { Num = n; Den = d; }
public static Rational operator *(Rational a, Rational b)
=> new Rational(a.Num * b.Num, a.Den * b.Den);
public override string ToString() => Num + "/" + Den;
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Rational(1, 2) * new Rational(2, 3));
}
}Dokumentuj przypadki brzegowe
Należy jasno określić, jak operatory obsługują wartości specjalne: dzielenie przez zero, operandy null w przypadku klas lub przepełnienie. Przewidywalna obsługa zapobiega zaskoczeniu kodu wywołującego.
using System;
public struct SafeDiv
{
public int Value;
public SafeDiv(int v) { Value = v; }
// Documented behavior: dividing by zero yields zero
public static SafeDiv operator /(SafeDiv a, SafeDiv b)
=> new SafeDiv(b.Value == 0 ? 0 : a.Value / b.Value);
public override string ToString() => Value.ToString();
}
public class Program
{
public static void Main()
{
Console.WriteLine(new SafeDiv(10) / new SafeDiv(0));
Console.WriteLine(new SafeDiv(10) / new SafeDiv(2));
}
}Zdyscyplinowany zestaw operatorów
Dopracowany typ numeryczny łączy kilka intuicyjnych operatorów, spójne porównywanie równości oraz niezmienność. W rezultacie zachowuje się jak wbudowana liczba i jest wygodny w użyciu.
using System;
public readonly struct Vec
{
public readonly double X, Y;
public Vec(double x, double y) { X = x; Y = y; }
public static Vec operator +(Vec a, Vec b) => new Vec(a.X + b.X, a.Y + b.Y);
public static Vec operator -(Vec a, Vec b) => new Vec(a.X - b.X, a.Y - b.Y);
public static Vec operator *(Vec a, double k) => new Vec(a.X * k, a.Y * k);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine((new Vec(1, 2) + new Vec(3, 4)) * 2);
}
}Wypróbuj samodzielnie
Należy połączyć wszystkie praktyki: niezmienny typ wartości, intuicyjne operatory, nazwaną alternatywę oraz spójne porównywanie równości.
using System;
public readonly struct Meters : IEquatable<Meters>
{
public readonly double Value;
public Meters(double v) { Value = v; }
public Meters Add(Meters other) => new Meters(Value + other.Value);
public static Meters operator +(Meters a, Meters b) => a.Add(b);
public bool Equals(Meters other) => Value == other.Value;
public override bool Equals(object obj) => obj is Meters m && Equals(m);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(Meters a, Meters b) => a.Equals(b);
public static bool operator !=(Meters a, Meters b) => !a.Equals(b);
public override string ToString() => Value + "m";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Meters(3) + new Meters(4));
Console.WriteLine(new Meters(7) == new Meters(7));
}
}Szybkie sprawdzenie
Należy zastosować dobre praktyki przeciążania operatorów.
Podsumowanie
Dobre przeciążanie operatorów jest zdyscyplinowane i przewidywalne.
- Operatory należy przeciążać tylko wtedy, gdy ich znaczenie jest oczywiste.
- Należy zachowywać oczekiwania matematyczne i symetrię.
- Operatory powinny pozostać czyste: zwracać nowe wartości i nigdy nie modyfikować operandów.
- Należy zachować zgodność
==zEquals/GetHashCode. - Warto udostępniać nazwane alternatywy i preferować typy przypominające wartości.
Często zadawane pytania
Czy lekcja „Dobre praktyki przeciążania operatorów” jest bezpłatna?
Tak — pełny tekst „Dobre praktyki przeciążania operatorów” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C# Academy, przejdź na CoddyKit PRO. Kurs C# Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Dobre praktyki przeciążania operatorów”?
Dbaj o intuicyjne działanie przeciążonych operatorów. Ćwiczysz C# Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć C# Academy?
Nie wymagamy żadnego doświadczenia. C# Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Dobre praktyki przeciążania operatorów”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji C# Academy?
Tak. Każda lekcja C# Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Przeciążanie operatorów arytmetycznych
- Przeciążanie operatorów porównania
- Konwersje definiowane przez użytkownika
- Dobre praktyki przeciążania operatorów