Zwracanie dwóch wartości, Tuple vs „out” vs „struct”

86

Rozważmy funkcję, która zwraca dwie wartości. Możemy pisać:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

Która z nich jest najlepszą praktyką i dlaczego?

Xaqron
źródło
Ciąg nie jest typem wartości. Myślę, że chciałeś powiedzieć „rozważ funkcję, która zwraca dwie wartości”.
Eric Lippert
@Eric: Masz rację. Miałem na myśli niezmienne typy.
Xaqron
a co jest nie tak z klasą?
Łukasz Madon
1
@lukas: Nic, ale na pewno nie jest to zgodne z najlepszymi praktykami. Jest to niewielka wartość (<16 KB) i jeśli dodam niestandardowy kod, skorzystam z tego, structjak Ericwspomniano.
Xaqron
1
Powiedziałbym, że używaj tylko wtedy, gdy potrzebujesz wartości zwracanej, aby zdecydować, czy w ogóle powinieneś przetwarzać dane zwracane, jak w TryParse, w przeciwnym razie zawsze powinieneś zwracać obiekt strukturalny, tak jakby obiekt strukturalny był typem wartości lub referencją typ zależy od tego, jak dodatkowo wykorzystasz dane
MikeT

Odpowiedzi:

93

Każdy z nich ma swoje wady i zalety.

Nasze parametry są szybkie i tanie, ale wymagają przekazania zmiennej i polegają na mutacji. Prawidłowe użycie parametru out z LINQ jest prawie niemożliwe.

Krotki powodują presję na wyrzucanie elementów bezużytecznych i nie dokumentują się samemu. „Pozycja 1” nie jest zbyt opisowa.

Struktury niestandardowe mogą być kopiowane powoli, jeśli są duże, ale samodokumentują się i są wydajne, jeśli są małe. Jednak zdefiniowanie całej grupy niestandardowych struktur do trywialnych zastosowań jest również uciążliwe.

Byłbym skłonny do niestandardowego rozwiązania strukturalnego, aby wszystkie inne rzeczy były takie same. Jeszcze lepiej jest jednak utworzyć funkcję, która zwraca tylko jedną wartość . Dlaczego w pierwszej kolejności zwracasz dwie wartości?

AKTUALIZACJA: Zwróć uwagę, że krotki w języku C # 7, które zostały wysłane sześć lat po napisaniu tego artykułu, są typami wartości i dlatego są mniej prawdopodobne, że będą powodować presję kolekcji.

Eric Lippert
źródło
2
Zwracanie dwóch wartości często zastępuje brak typów opcji lub ADT.
Anton Tykhyy
2
Z mojego doświadczenia z innymi językami powiedziałbym, że generalnie krotki są używane do szybkiego i nieczystego grupowania elementów. Zwykle lepiej jest utworzyć klasę lub strukturę po prostu dlatego, że umożliwia nazwanie każdego elementu. Podczas korzystania z krotek znaczenie każdej wartości może być trudne do określenia. Ale oszczędza ci to czasu na tworzenie klasy / struktury, która może być przesadzona, jeśli ta klasa / struktura nie byłaby używana gdzie indziej.
Kevin Cathcart,
23
@Xaqron: Jeśli stwierdzisz, że idea „danych z limitem czasu” jest powszechna w Twoim programie, możesz rozważyć utworzenie typu ogólnego „TimeLimited <T>”, aby metoda zwracała TimeLimited <string> lub TimeLimited <Uri> lub cokolwiek. Klasa TimeLimited <T> może wtedy mieć metody pomocnicze, które informują „jak długo nam zostało?” lub „czy wygasło?” lub cokolwiek. Spróbuj uchwycić takie interesujące semantyki w systemie czcionek.
Eric Lippert
3
Absolutnie nigdy nie użyłbym Tuple jako części interfejsu publicznego. Ale nawet w przypadku kodu „prywatnego” uzyskuję ogromną czytelność dzięki właściwemu typowi zamiast używania Tuple (szczególnie, jak łatwo jest utworzyć prywatny typ wewnętrzny z Auto Properties).
Rozwiązanie
2
Co oznacza ciśnienie zbierania?
rolki
25

Dodając do poprzednich odpowiedzi, C # 7 wprowadza krotki typów wartości, w przeciwieństwie do System.Tupletego typu referencyjnego, a także oferuje ulepszoną semantykę.

Nadal możesz pozostawić je bez nazw i użyć .Item*składni:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Ale to, co jest naprawdę potężne w tej nowej funkcji, to możliwość nazwania krotek. Moglibyśmy więc przepisać powyższe w ten sposób:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

Obsługiwana jest również destrukcja:

(string firstName, string lastName, int age) = getPerson()

dimlucas
źródło
2
Czy mam rację, myśląc, że to zwraca w zasadzie strukturę z odniesieniami jako członkami pod maską?
Austin_Anderson,
4
czy wiemy, jak wypada to w porównaniu z wykorzystaniem naszych parametrów?
SpaceMonkey
20

Myślę, że odpowiedź zależy od semantyki tego, co robi funkcja i relacji między tymi dwiema wartościami.

Na przykład TryParsemetody przyjmują outparametr, który akceptuje przeanalizowaną wartość, i zwracają znak, boolaby wskazać, czy analiza powiodła się. Te dwie wartości tak naprawdę nie pasują do siebie, więc semantycznie ma więcej sensu, a intencja kodu jest łatwiejsza do odczytania, użycie outparametru.

Jeśli jednak twoja funkcja zwraca współrzędne X / Y jakiegoś obiektu na ekranie, to te dwie wartości semantycznie należą do siebie i lepiej byłoby użyć struct.

Osobiście unikałbym używania a tuplefor cokolwiek, co będzie widoczne dla kodu zewnętrznego ze względu na niezręczną składnię pobierania członków.

Andrew Cooper
źródło
+1 za semantykę. Twoja odpowiedź jest bardziej odpowiednia z typami referencyjnymi, gdy możemy zostawić outparametr null. Istnieje kilka niezmiennych typów, których wartość dopuszcza wartość zerową.
Xaqron
3
W rzeczywistości dwie wartości w TryParse bardzo należą do siebie, znacznie bardziej niż to wynika z posiadania jednej jako wartości zwracanej, a drugiej jako parametru ByRef. Pod wieloma względami logiczną rzeczą do zwrócenia byłby typ dopuszczający wartość null. W niektórych przypadkach wzorzec TryParse działa dobrze, a w innych jest uciążliwy (fajnie, że można go użyć w instrukcji „if”, ale jest wiele przypadków, w których zwracanie wartości null lub możliwość określenia wartości domyślnej byłoby wygodniejsze).
supercat
@supercat Zgodziłbym się z Andrew, nie pasują do siebie. chociaż są one powiązane, zwrot mówi ci, czy w ogóle musisz zawracać sobie głowę wartością, a nie czymś, co musi być przetwarzane w tandemie. więc po przetworzeniu zwrotu nie jest on już potrzebny do żadnego innego przetwarzania związanego z wartością out, jest to inne niż powiedzenie zwrócenia KeyValuePair ze słownika, w którym istnieje wyraźne i ciągłe łącze między kluczem a wartością. chociaż zgadzam się, że jeśli typy dopuszczające wartość null byłyby w .Net 1.1, prawdopodobnie
użyliby
@MikeT: Uważam za wyjątkowo niefortunne, że Microsoft zasugerował, że struktury powinny być używane tylko do rzeczy reprezentujących pojedynczą wartość, podczas gdy w rzeczywistości struktura z odsłoniętym polem jest idealnym medium do przekazywania razem grupy niezależnych zmiennych połączonych taśmą klejącą . Wskaźnik sukcesu i wartość mają znaczenie razem w momencie zwrócenia , nawet jeśli później byłyby bardziej przydatne jako oddzielne zmienne. Pola o strukturze ujawnionych pól przechowywane w zmiennej same mogą być używane jako oddzielne zmienne. W każdym razie ...
supercat
@MikeT: Ze względu na sposób, w jaki kowariancja jest i nie jest obsługiwana w ramach frameworka, jedynym trywzorcem, który działa z kowariantnymi interfejsami jest T TryGetValue(whatever, out bool success); że podejście pozwoliłoby interfejsy IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>i pozwól kod, który chce map instancji Animaldo instancji Cardo zaakceptowania Dictionary<Cat, ToyotaCar>[użyciem TryGetValue<TKey>(TKey key, out bool success). Taka wariancja nie jest możliwa, jeśli TValuezostanie użyta jako refparametr.
supercat
2

Pójdę za podejściem z użyciem parametru Out, ponieważ w drugim podejściu wymagałoby to utworzenia i obiektu klasy Tuple, a następnie dodania do niej wartości, co moim zdaniem jest kosztowną operacją w porównaniu do zwracania wartości z parametru out. Chociaż jeśli chcesz zwrócić wiele wartości w klasie Tuple (których w rzeczywistości nie można osiągnąć przez zwrócenie tylko jednego parametru wyjściowego), przejdę do drugiego podejścia.

Sumit
źródło
Zgadzam się z out. Dodatkowo istnieje paramssłowo kluczowe, o którym nie wspomniałem, aby odpowiedzieć na pytanie.
Xaqron
2

Nie wspomniałeś o jeszcze jednej opcji, którą jest posiadanie niestandardowej klasy zamiast struktury. Jeśli dane mają skojarzoną semantykę, na której mogą być obsługiwane funkcje, lub rozmiar instancji jest wystarczająco duży (z reguły> 16 bajtów), preferowana może być klasa niestandardowa. Używanie „out” nie jest zalecane w publicznym interfejsie API ze względu na jego powiązanie ze wskaźnikami i wymagające zrozumienia, jak działają typy referencyjne.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Krotka jest dobra do użytku wewnętrznego, ale jest niewygodna w publicznym interfejsie API. Tak więc mój głos jest między strukturą a klasą dla publicznego interfejsu API.

JeanP
źródło
1
Jeśli typ istnieje wyłącznie w celu zwrócenia agregacji wartości, powiedziałbym, że prosty typ wartości pola odsłoniętego jest najlepiej dopasowany do tej semantyki. Jeśli w typie nie ma nic poza polami, nie będzie żadnego pytania o to, jakie rodzaje walidacji danych wykonuje (oczywiście żadnych), czy reprezentuje widok przechwycony czy na żywo (struktura z otwartym polem nie może działać jako podgląd na żywo), itp. Niezmienne klasy są mniej wygodne w użyciu i zapewniają korzyści w zakresie wydajności tylko wtedy, gdy wystąpienia można przekazywać wiele razy.
supercat
0

Nie ma „najlepszych praktyk”. To jest to, z czym czujesz się komfortowo i co działa najlepiej w Twojej sytuacji. Jeśli jesteś z tym konsekwentny, nie ma problemu z żadnym z opublikowanych przez Ciebie rozwiązań.

lisi
źródło
3
Oczywiście, że wszystkie działają. Na wypadek, gdyby nie było przewagi technicznej, nadal jestem ciekawy, co jest najczęściej używane przez ekspertów.
Xaqron