Czy ktoś może mi powiedzieć, czy istnieje sposób, aby za pomocą generyków ograniczyć argument typu ogólnego T
do tylko:
Int16
Int32
Int64
UInt16
UInt32
UInt64
Znam where
słowo kluczowe, ale nie mogę znaleźć interfejsu tylko dla tych typów,
Coś jak:
static bool IntegerFunction<T>(T value) where T : INumeric
c#
generics
constraints
Corin Blaikie
źródło
źródło
Odpowiedzi:
C # nie obsługuje tego. Hejlsberg opisał powody niewdrożenia tej funkcji w wywiadzie dla Bruce'a Eckela :
Prowadzi to jednak do dość skomplikowanego kodu, w którym użytkownik musi dostarczyć własną
Calculator<T>
implementację dla każdegoT
, którego chce użyć. O ile nie musi to być rozszerzalne, tj. Jeśli chcesz obsługiwać określoną liczbę typów, takich jakint
idouble
, możesz uciec ze stosunkowo prostym interfejsem:( Minimalna implementacja w GitHub Gist. )
Jednak gdy tylko chcesz, aby użytkownik mógł dostarczyć własne, niestandardowe typy, musisz otworzyć tę implementację, aby użytkownik mógł dostarczyć własne
Calculator
wystąpienia. Na przykład, aby utworzyć instancję macierzy korzystającej z niestandardowej dziesiętnej implementacji zmiennoprzecinkowejDFP
, musisz napisać ten kod:… I zaimplementuj wszystkich członków
DfpCalculator : ICalculator<DFP>
.Alternatywą, która niestety łączy te same ograniczenia, jest praca z klasami polityki, jak omówiono w odpowiedzi Siergieja Shandara .
źródło
Operator
/Operator<T>
; yoda.arachsys.com/csharp/miscutil/usage/genericoperators.htmlOperator<T>
kodzie (ponieważ wywiad został udzielony na długo przed istnieniemExpressions
frameworka, chociaż można by Oczywiście użycieReflection.Emit
) - i byłbym bardzo zainteresowany jego obejścia.Biorąc pod uwagę popularność tego pytania i zainteresowanie taką funkcją, jestem zaskoczony, widząc, że nie ma jeszcze odpowiedzi dotyczącej T4.
W tym przykładowym kodzie pokażę bardzo prosty przykład, w jaki sposób można wykorzystać potężny silnik szablonów do robienia tego, co kompilator właściwie robi za kulisami za pomocą generyków.
Zamiast przechodzić przez obręcze i poświęcać pewność kompilacji, możesz po prostu wygenerować pożądaną funkcję dla każdego typu, który ci się podoba i użyć jej odpowiednio (w czasie kompilacji!).
Aby to zrobić:
Otóż to. Już skończyłeś.
Zapisanie tego pliku automatycznie skompiluje go do tego pliku źródłowego:
W swojej
main
metodzie możesz sprawdzić, czy masz pewność kompilacji:Wyprzedzę jedną uwagę: nie, to nie jest naruszenie zasady SUSZENIA. Zasada DRY ma na celu zapobieganie powielaniu kodu w wielu miejscach, co utrudniłoby utrzymanie aplikacji.
W tym przypadku wcale tak nie jest: jeśli chcesz zmiany, możesz po prostu zmienić szablon (jedno źródło dla całego pokolenia!) I gotowe.
Aby użyć go z własnymi niestandardowymi definicjami, dodaj deklarację przestrzeni nazw (upewnij się, że jest taka sama jak ta, w której zdefiniujesz własną implementację) do wygenerowanego kodu i oznacz klasę jako
partial
. Następnie dodaj te wiersze do pliku szablonu, aby został włączony do ostatecznej kompilacji:Bądźmy szczerzy: to całkiem fajne.
Oświadczenie: Kevin Hazzard i Jason Bock, Manning Publications, na tę próbkę mieli duży wpływ Metaprogramowanie w .NET .
źródło
T
który jest lub dziedziczy z różnychIntX
klas? Podoba mi się to rozwiązanie, ponieważ oszczędza czas, ale aby w 100% rozwiązać problem (mimo że nie jest tak przyjemny, jakby C # obsługiwał tego typu ograniczenia, wbudowane), każda z wygenerowanych metod powinna być ogólna, aby mogą zwrócić obiekt typu, który dziedziczy po jednej zIntXX
klas.IntXX
typy są strukturami, co oznacza, że nie obsługują dziedziczenia . A nawet gdyby tak się stało wówczas zasadę substytucji Liskov (który może znasz ze stałego idiomu) obowiązuje: jeśli metoda jest zdefiniowana jakoX
iY
jest dzieckiemX
wtedy z definicji każdyY
powinien móc być przekazywane do tej metody jako substytut jego typ podstawowy.Nie ma na to żadnych ograniczeń. To prawdziwy problem dla każdego, kto chce używać ogólnych danych do obliczeń numerycznych.
Poszedłbym dalej i powiedział, że potrzebujemy
Lub nawet
Niestety masz tylko interfejsy, klasy podstawowe i słowa kluczowe
struct
(musi być typem wartości),class
(musi być typem referencyjnym) inew()
(musi mieć domyślny konstruktor)Możesz zawinąć liczbę w coś innego (podobnego do
INullable<T>
) jak tutaj w projekcie kodowym .Możesz zastosować ograniczenie w czasie wykonywania (przez zastanawianie się nad operatorami lub sprawdzanie typów), ale to nie ma przewagi, że generyczne jest na pierwszym miejscu.
źródło
where T : operators( +, -, /, * )
jest legalny C #? Przepraszamy za pytanie dla początkujących.where T : operators( +, -, /, * )
, ale nie możemy.Obejście przy użyciu zasad:
Algorytmy:
Stosowanie:
Rozwiązanie jest bezpieczne podczas kompilacji. CityLizard Framework zapewnia skompilowaną wersję .NET 4.0. Plik to lib / NETFramework4.0 / CityLizard.Policy.dll.
Jest również dostępny w Nuget: https://www.nuget.org/packages/CityLizard/ . Zobacz strukturę CityLizard.Policy.I .
źródło
struct
? co jeśli użyję zamiast tego klasy singleton i zmienię instancję na,public static NumericPolicies Instance = new NumericPolicies();
a następnie dodam ten konstruktorprivate NumericPolicies() { }
.T Add<T> (T t1, T t2)
, aleSum()
działa tylko wtedy, gdy może odzyskać swój własny typ T z jego parametrów, co nie jest możliwe gdy jest osadzony w innej funkcji ogólnej.To pytanie jest trochę FAQ, więc zamieszczam je jako wiki (ponieważ pisałem wcześniej podobne, ale jest to starsze); tak czy siak...
Jakiej wersji .NET używasz? Jeśli używasz .NET 3.5, to mam ogólną implementację operatorów w MiscUtil (darmowy itp.).
Ma to metody takie jak
T Add<T>(T x, T y)
i inne warianty arytmetyki na różnych typach (jakDateTime + TimeSpan
).Dodatkowo działa to dla wszystkich wbudowanych, podniesionych i wykonanych na zamówienie operatorów oraz buforuje delegata pod kątem wydajności.
Dodatkowe informacje na temat tego, dlaczego jest to trudne, znajdują się tutaj .
Możesz także wiedzieć, że
dynamic
(4.0) rozwiązuje ten problem również pośrednio - tjźródło
Niestety w tym przypadku możesz określić struct tylko w klauzuli where. Wydaje się dziwne, że nie można dokładnie określić Int16, Int32 itp., Ale jestem pewien, że istnieje głęboka przyczyna implementacji leżąca u podstaw decyzji o niedopuszczeniu typów wartości w klauzuli where.
Wydaje mi się, że jedynym rozwiązaniem jest sprawdzenie środowiska uruchomieniowego, co niestety zapobiega wykrywaniu problemu w czasie kompilacji. To pójdzie coś takiego:
Co jest trochę brzydkie, wiem, ale przynajmniej zapewnia wymagane ograniczenia.
Zastanowiłbym się również nad możliwymi implikacjami dotyczącymi wydajności dla tej implementacji, być może istnieje szybsze wyjście.
źródło
// Rest of code...
może się nie kompilować, jeśli zależy to od operacji zdefiniowanych przez ograniczenia.// Rest of code...
podobnyvalue + value
lubvalue * value
, masz błąd kompilacji.Prawdopodobnie najbliższe jest to, co możesz zrobić
Nie jestem pewien, czy możesz wykonać następujące czynności
W przypadku czegoś tak specyficznego, dlaczego nie mieć przeciążeń dla każdego typu, lista jest tak krótka i prawdopodobnie zajmowałaby mniej miejsca w pamięci.
źródło
Począwszy od wersji C # 7.3, można użyć dokładniejszego przybliżenia - ograniczenia niezarządzanego w celu określenia, że parametr typu jest niezarządzalnym wskaźnikiem, nie dopuszczającym wartości zerowej .
Wiązanie niezarządzane implikuje ograniczenie struktury i nie może być łączone z ograniczeniami struct ani new ().
Typ jest typem niezarządzanym, jeśli jest jednym z następujących typów:
Aby dodatkowo ograniczyć i wyeliminować typy wskaźników i zdefiniowane przez użytkownika, które nie implementują IComparable, dodaj IComparable (ale wyliczanie nadal pochodzi z IComparable, więc ogranicz wyliczanie przez dodanie IEquatable <T>, możesz pójść dalej w zależności od okoliczności i dodać dodatkowe interfejsy. niezarządzany pozwala skrócić tę listę):
źródło
DateTime
podlegaunmanaged, IComparable, IEquatable<T>
ograniczeniom ...Nie ma możliwości ograniczenia szablonów do typów, ale można zdefiniować różne działania w zależności od typu. W ramach ogólnego pakietu numerycznego potrzebowałem klasy ogólnej, aby dodać dwie wartości.
Zauważ, że typeofs są oceniane w czasie kompilacji, więc instrukcje if zostaną usunięte przez kompilator. Kompilator usuwa również fałszywe rzutowania. Coś więc rozwiązałoby się w kompilatorze
źródło
Stworzyłem małą funkcjonalność biblioteki, aby rozwiązać te problemy:
Zamiast:
Możesz napisać:
Możesz znaleźć kod źródłowy tutaj: /codereview/26022/improvement-requested-for-generic-calculator-and-generic-number
źródło
Zastanawiałem się tak samo jak samjudson, dlaczego tylko liczby całkowite? a jeśli tak jest, możesz utworzyć klasę pomocnika lub coś takiego, aby pomieścić wszystkie typy, które chcesz.
Jeśli wszystko, czego chcesz, to liczby całkowite, nie używaj ogólnego, który nie jest ogólny; lub jeszcze lepiej, odrzuć dowolny inny typ, sprawdzając jego typ.
źródło
Nie ma na to „dobrego” rozwiązania. Jednak można znacznie zawęzić argument typu, aby wykluczyć wiele niedopasowań związanych z hipotetycznym ograniczeniem „INumeric”, jak pokazał Haacked powyżej.
static bool IntegerFunction <T> (wartość T) gdzie T: IComparable, IFormattable, IConvertible, IComparable <T>, IEquatable <T>, struct {...
źródło
Jeśli używasz .NET 4.0 i nowszych wersji, możesz po prostu użyć argumentu dynamicznego jako argumentu metody i sprawdzić w środowisku wykonawczym , czy przekazany typ argumentu dynamicznego jest typu liczbowego / liczb całkowitych.
Jeśli typ przekazywanej dynamiki nie jest typem liczbowym / całkowitym, wyrzuć wyjątek.
Przykładowy krótki kod, który implementuje ten pomysł, to coś takiego:
Oczywiście, że to rozwiązanie działa tylko w czasie wykonywania, ale nigdy w czasie kompilacji.
Jeśli chcesz rozwiązania, które zawsze działa w czasie kompilacji, a nigdy w czasie wykonywania, będziesz musiał zawinąć dynamikę w publiczną strukturę / klasę, której przeciążone publiczne konstruktory akceptują tylko argumenty pożądanych typów i nadają struktury / klasie odpowiednią nazwę.
Ma sens, że zawinięta dynamika jest zawsze prywatnym członkiem klasy / struct i jest jedynym członkiem struct / class, a nazwa jedynego członka struct / class to „wartość”.
Będziesz także musiał zdefiniować i wdrożyć publiczne metody i / lub operatory, które działają z pożądanymi typami dla prywatnego dynamicznego członka klasy / struktury, jeśli to konieczne.
Ma to również sens, że struct / klasa ma specjalny / unikalny konstruktor, który akceptuje dynamikę jako argument inicjujący tylko prywatny element dynamiczny o nazwie „wartość”, ale modyfikator tego konstruktora jest oczywiście prywatny .
Gdy klasa / struktura będzie gotowa, zdefiniuj typ argumentu IntegerFunction, aby była to klasa / struktura, która została zdefiniowana.
Przykładowy długi kod, który implementuje ten pomysł, to coś takiego:
Pamiętaj, że aby użyć dynamiki w kodzie, musisz dodać odniesienie do Microsoft.CSharp
Jeśli wersja .NET Framework jest niższa / niższa / mniejsza niż 4.0, a dynamika nie jest zdefiniowana w tej wersji, będziesz musiał zamiast tego użyć obiektu i rzutować na liczbę całkowitą, co jest problemem, więc polecam użyć w przynajmniej .NET 4.0 lub nowszy, jeśli możesz, dzięki czemu możesz używać dynamicznego zamiast obiektowego .
źródło
Niestety .NET nie zapewnia tego w sposób natywny.
Aby rozwiązać ten problem, stworzyłem bibliotekę OSS Genumerics, która zapewnia większość standardowych operacji numerycznych dla następujących wbudowanych typów liczbowych i ich zerowalnych odpowiedników z możliwością dodania obsługi innych typów liczbowych.
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,float
,double
,decimal
, IBigInteger
Wydajność jest równoważna rozwiązaniu numerycznemu, umożliwiającemu tworzenie wydajnych ogólnych algorytmów numerycznych.
Oto przykład użycia kodu.
źródło
Jaki jest sens tego ćwiczenia?
Jak już zauważyli ludzie, możesz mieć nietypową funkcję zajmującą największy element, a kompilator automatycznie skonwertuje dla ciebie mniejsze int.
Jeśli twoja funkcja znajduje się na ścieżce krytycznej pod względem wydajności (bardzo mało prawdopodobne, IMO), możesz spowodować przeciążenie wszystkich potrzebnych funkcji.
źródło
Chciałbym użyć ogólnego, który można obsługiwać zewnętrznie ...
źródło
To ograniczenie dotyczyło mnie, gdy próbowałem przeciążać operatorów dla typów ogólnych; ponieważ nie istniało ograniczenie „INumeric”, a z wielu innych powodów dobrzy ludzie na przepływie stosu chętnie go udostępniają, operacji nie można definiować na typach ogólnych.
Chciałem coś takiego
Rozwiązałem ten problem, używając dynamicznego pisania w środowisku uruchomieniowym .net4.
Dwie rzeczy o użyciu
dynamic
sąźródło
Prymitywne typy liczbowe .NET nie mają wspólnego interfejsu, który pozwalałby na ich użycie do obliczeń. Byłoby to możliwe, aby zdefiniować własne interfejsy (np
ISignedWholeNumber
), które wykonują takie operacje, definiujące struktury, które zawierają jedenInt16
,Int32
itp i wdrożenia tych interfejsów, a następnie mają metody, które przyjmują typy generyczne ograniczona doISignedWholeNumber
, ale konieczności konwertowania wartości liczbowych dla twoich typów konstrukcji prawdopodobnie byłby uciążliwy.Alternatywnym rozwiązaniem byłoby zdefiniowanie statycznych klasy
Int64Converter<T>
z właściwości statycznychbool Available {get;};
i statycznych delegatówInt64 GetInt64(T value)
,T FromInt64(Int64 value)
,bool TryStoreInt64(Int64 value, ref T dest)
. Konstruktor klas mógłby użyć zakodowanego na stałe ładowania ładunków delegowanych dla znanych typów i ewentualnie użyć Reflection w celu przetestowania, czy typT
implementuje metody o odpowiednich nazwach i podpisach (w przypadku, gdy jest to coś w rodzaju struktury, która zawieraInt64
i reprezentuje liczbę, ale maToString()
metoda niestandardowa ). Takie podejście straciłoby zalety związane z sprawdzaniem typu podczas kompilacji, ale nadal udałoby mu się uniknąć operacji bokserskich, a każdy typ musiałby być „sprawdzony” tylko raz. Następnie operacje powiązane z tym typem zostaną zastąpione wysyłką delegowaną.źródło
Int64
Wyniku, ale nie zapewnia sposobu, w jaki można np. Zwiększyć liczbę całkowitą dowolnego typu, aby uzyskać inną liczbę całkowitą tego samego typu .Miałem podobną sytuację, w której musiałem obsługiwać typy numeryczne i ciągi; wydaje się to trochę dziwne połączenie, ale proszę bardzo.
Ponownie, jak wiele osób, spojrzałem na ograniczenia i wymyśliłem kilka interfejsów, które musiał obsługiwać. Jednak: a) nie było w 100% wodoszczelne ib) każdy, kto spojrzy na tę długą listę ograniczeń, będzie od razu bardzo zdezorientowany.
Więc moje podejście polegało na umieszczeniu całej mojej logiki w ogólnej metodzie bez żadnych ograniczeń, ale uczynieniu tej ogólnej metody prywatną. Następnie odsłoniłem go metodami publicznymi, z których jeden wyraźnie obsługuje typ, który chciałem obsłużyć - według mnie kod jest czysty i wyraźny, np.
źródło
Jeśli wszystko, czego potrzebujesz, to użyć jednego typu liczbowego , możesz rozważyć utworzenie czegoś podobnego do aliasu w C ++ za pomocą
using
.Więc zamiast mieć bardzo ogólny
mógłbyś mieć
To może pozwoli Ci łatwo przejść od
double
celuint
lub inne, jeśli to konieczne, ale nie będzie w stanie korzystaćComputeSomething
zdouble
iint
w tym samym programie.Ale dlaczego nie zastąpić wszystkich
double
doint
tego czasu? Ponieważ twoja metoda może chcieć użyćdouble
parametru wejściowegodouble
lubint
. Alias pozwala dokładnie wiedzieć, która zmienna używa typu dynamicznego .źródło
Temat jest stary, ale dla przyszłych czytelników:
Ta funkcja jest ściśle związana z tym,
Discriminated Unions
co do tej pory nie zostało zaimplementowane w języku C #. Znalazłem jego problem tutaj:https://github.com/dotnet/csharplang/issues/113
Ten problem jest nadal otwarty, a funkcja została zaplanowana
C# 10
Musimy jeszcze trochę poczekać, ale po wydaniu możesz to zrobić w ten sposób:
źródło
Myślę, że nie rozumiesz generycznych. Jeśli operacja, którą próbujesz wykonać, jest dobra tylko dla określonych typów danych, to nie robisz czegoś „ogólnego”.
Ponadto, ponieważ chcesz zezwolić tej funkcji tylko na int typy danych, nie powinieneś potrzebować osobnej funkcji dla każdego określonego rozmiaru. Po prostu przyjęcie parametru w największym konkretnym typie pozwoli programowi automatycznie przesłać do niego mniejsze typy danych. (tzn. przekazanie Int16 spowoduje automatyczną konwersję na Int64 podczas połączenia).
Jeśli wykonujesz różne operacje w oparciu o rzeczywisty rozmiar int przekazywany do funkcji, pomyślałem, że powinieneś poważnie przemyśleć nawet próbę zrobienia tego, co robisz. Jeśli musisz oszukać język, powinieneś pomyśleć nieco więcej o tym, co próbujesz osiągnąć, niż o tym, co chcesz zrobić.
W przeciwnym razie można użyć parametru typu Object, a następnie trzeba będzie sprawdzić typ parametru i podjąć odpowiednie działanie lub zgłosić wyjątek.
źródło