Jaka jest najgorsza gotcha w C # lub .NET? [Zamknięte]


377

Niedawno pracowałem z DateTimeobiektem i napisałem coś takiego:

DateTime dt = DateTime.Now;
dt.AddDays(1);
return dt; // still today's date! WTF?

Dokumentacja intellisense AddDays()mówi, że dodaje dzień do daty, a tak nie jest - w rzeczywistości zwraca datę z dodanym dniem, więc musisz napisać:

DateTime dt = DateTime.Now;
dt = dt.AddDays(1);
return dt; // tomorrow's date

Ten raz mnie ugryzł wiele razy, więc pomyślałem, że przydałoby się skatalogować najgorsze gotówki C #.


157
return DateTime.Now.AddDays (1);
crashmstr

23
AFAIK, wszystkie wbudowane typy wartości są niezmienne, przynajmniej w tym sensie, że każda metoda zawarta w tym typie zwraca nowy element zamiast modyfikować istniejący element. Przynajmniej nie mogę wymyślić takiego, który by tego nie robił: wszystko miłe i konsekwentne.
Joel Coehoorn

6
Zmienna typ wartości: System.Collections.Generics.List.Enumerator :( (I tak, możesz zobaczyć, że zachowuje się dziwnie, jeśli spróbujesz wystarczająco mocno.)
Jon Skeet

13
Intellisense zapewnia wszystkie potrzebne informacje. Mówi, że zwraca obiekt DateTime. Gdyby tylko zmienił ten, który przekazałeś, byłaby to nieważna metoda.
John Kraft,

20
Niekoniecznie: StringBuilder.Append (...) zwraca na przykład „to”. Jest to dość powszechne w płynnych interfejsach.
Jon Skeet

Odpowiedzi:


304
private int myVar;
public int MyVar
{
    get { return MyVar; }
}

Blammo. Aplikacja ulega awarii bez śladu stosu. Dzieje się cały czas.

(Zwróć uwagę na kapitał MyVarzamiast małych liter myVarw getterze).


112
i SO odpowiednie dla tej strony :)
gbjbaanb

62
Podkreślam prywatnego członka, bardzo pomaga!
czakryt

61
Korzystam z automatycznych właściwości tam, gdzie mogę, ale zatrzymuje ten problem;)
TWith2Sugars,

28
To jest WIELKI powód, aby używać prefiksów dla swoich prywatnych pól (są inne, ale to jest dobre): _myVar, m_myVar
jrista

205
@jrista: O proszę NIE ... nie m_ ... aargh horror ...
fretje

254

Type.GetType

Ten, który widziałem, gryzie wiele osób Type.GetType(string). Zastanawiają się, dlaczego to działa dla typów w ich własnym zestawie, a niektóre typy lubią System.String, ale nie System.Windows.Forms.Form. Odpowiedź jest taka, że ​​wygląda tylko w bieżącym zestawie i w mscorlib.


Anonimowe metody

C # 2.0 wprowadził anonimowe metody, prowadząc do takich nieprzyjemnych sytuacji:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            ThreadStart ts = delegate { Console.WriteLine(i); };
            new Thread(ts).Start();
        }
    }
}

Co to wydrukuje? Cóż, to całkowicie zależy od harmonogramu. Wydrukuje 10 liczb, ale prawdopodobnie nie wydrukuje 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, czego można się spodziewać. Problem polega na tym, że izostała przechwycona zmienna, a nie jej wartość w momencie tworzenia delegata. Można to łatwo rozwiązać za pomocą dodatkowej zmiennej lokalnej o odpowiednim zakresie:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            ThreadStart ts = delegate { Console.WriteLine(copy); };
            new Thread(ts).Start();
        }
    }
}

Odroczone wykonanie bloków iteratora

Ten „test jednostkowy biednego człowieka” nie przechodzi - dlaczego nie?

using System;
using System.Collections.Generic;
using System.Diagnostics;

class Test
{
    static IEnumerable<char> CapitalLetters(string input)
    {
        if (input == null)
        {
            throw new ArgumentNullException(input);
        }
        foreach (char c in input)
        {
            yield return char.ToUpper(c);
        }
    }

    static void Main()
    {
        // Test that null input is handled correctly
        try
        {
            CapitalLetters(null);
            Console.WriteLine("An exception should have been thrown!");
        }
        catch (ArgumentNullException)
        {
            // Expected
        }
    }
}

Odpowiedź jest taka, że ​​kod w źródle CapitalLetterskodu nie zostanie wykonany, dopóki nie MoveNext()zostanie wywołana metoda iteratora .

Na mojej łamigłówce są jeszcze inne dziwactwa .


25
Przykład iteratora jest przebiegły!
Jimmy

8
dlaczego nie podzielić tego na 3 odpowiedzi, abyśmy mogli głosować każdy zamiast wszystkich razem?
chakrit

13
@chakrit: Z perspektywy czasu byłby to prawdopodobnie dobry pomysł, ale myślę, że jest już za późno. Mogło to również wyglądać tak, jakbym chciał uzyskać więcej przedstawicieli ...
Jon Skeet

19
W rzeczywistości Type.GetType działa, jeśli podasz AssemblyQualifiedName. Type.GetType ("System.ServiceModel.EndpointNotFoundException, System.ServiceModel, Wersja = 3.0.0.0, Kultura = neutralna, PublicKeyToken = b77a5c561934e089");
chilltemp

2
@kentaromiura: Rozdzielczość przeciążenia zaczyna się od najbardziej pochodnego typu i działa na drzewie - ale tylko patrząc na metody pierwotnie zadeklarowane w typie, na który patrzy. Foo (int) zastępuje metodę podstawową, więc nie jest brane pod uwagę. Obowiązuje Foo (obiekt), więc zatrzymuje się tam przeciążenie. Dziwne, wiem.
Jon Skeet

194

Ponowne zgłaszanie wyjątków

Gotcha, która dostaje wielu nowych programistów, to semantyka wyjątków ponownego rzucania.

Dużo czasu widzę następujący kod

catch(Exception e) 
{
   // Do stuff 
   throw e; 
}

Problem polega na tym, że czyści ślad stosu i znacznie utrudnia diagnozowanie problemów, ponieważ nie można śledzić, skąd pochodzi wyjątek.

Poprawnym kodem jest albo instrukcja throw bez argumentów:

catch(Exception)
{
    throw;
}

Lub zawinięcie wyjątku w inny i użycie wewnętrznego wyjątku, aby uzyskać oryginalny ślad stosu:

catch(Exception e) 
{
   // Do stuff 
   throw new MySpecialException(e); 
}

Na szczęście ktoś mnie o tym dowiedział w pierwszym tygodniu i znalazłem go w kodzie dla starszych programistów. Is: catch () {throw; } To samo co drugi fragment kodu? catch (Exception e) {throw; } tylko to nie tworzy obiektu wyjątku i nie wypełnia go?
StuperUser

Oprócz błędu użycia rzutu ex (lub rzutu e) zamiast po prostu rzutu, muszę się zastanawiać, jakie są przypadki, w których warto złapać wyjątek tylko po to, aby go rzucić ponownie.
Ryan Lundy,

13
@ Kyralessa: istnieje wiele przypadków: na przykład, jeśli chcesz wycofać transakcję, zanim dzwoniący otrzyma wyjątek. Cofasz się, a następnie ponownie rzucasz.
R. Martinho Fernandes,

7
Widzę to przez cały czas, gdy ludzie łapią i odrzucają wyjątki tylko dlatego, że uczy się ich, że muszą wychwytywać wszystkie wyjątki, nie zdając sobie sprawy, że zostanie to złapane dalej na stosie wywołań. Doprowadza mnie to do szału.
James Westgate

5
@ Kyralessa największym przypadkiem jest, gdy musisz zalogować się. Zaloguj błąd w catch i
wyrzuć

194

Okno obserwacyjne Heisenberga

To może cię źle ugryźć, jeśli robisz rzeczy na żądanie, takie jak to:

private MyClass _myObj;
public MyClass MyObj {
  get {
    if (_myObj == null)
      _myObj = CreateMyObj(); // some other code to create my object
    return _myObj;
  }
}

Powiedzmy teraz, że masz jakiś kod gdzie indziej, używając tego:

// blah
// blah
MyObj.DoStuff(); // Line 3
// blah

Teraz chcesz debugować swoją CreateMyObj()metodę. Więc umieściłeś punkt przerwania na linii 3 powyżej, z zamiarem wkroczenia do kodu. Na wszelki wypadek umieściłeś również punkt przerwania na linii powyżej _myObj = CreateMyObj();, a nawet punkt przerwania w CreateMyObj()sobie.

Kod uderza Twój punkt przerwania na linii 3. Wchodzisz do kodu. Oczekujesz wprowadzenia kodu warunkowego, ponieważ _myObjjest oczywiście zerowy, prawda? Uh ... więc ... dlaczego to pominęło warunek i poszło prosto do return _myObj?! Najedziesz myszką na _myObj ... i rzeczywiście ma wartość! Jak to się stało?!

Odpowiedź jest taka, że ​​twoje IDE spowodowało, że uzyskało wartość, ponieważ masz otwarte okno „obserwuj” - zwłaszcza okno obserwacyjne „Autos”, które wyświetla wartości wszystkich zmiennych / właściwości odpowiednich dla bieżącego lub poprzedniego wiersza wykonywania. Kiedy osiągniesz punkt przerwania na linii 3, okno obserwacyjne zdecydowało, że będziesz zainteresowany, aby poznać wartość MyObj- więc za kulisami, ignorując dowolny z punktów przerwania , poszedł i obliczył MyObjdla ciebie wartość - w tym wezwanie do CreateMyObj()tego ustawia wartość _myObj!

Dlatego nazywam to oknem zegarka Heisenberga - nie można obserwować wartości bez wpływu na nią ... :)

GOTCHA!


Edycja - Uważam, że komentarz @ ChristianHayter zasługuje na uwzględnienie w głównej odpowiedzi, ponieważ wygląda na skuteczne obejście tego problemu. Więc zawsze, gdy masz leniwie załadowaną właściwość ...

Udekoruj swoją właściwość za pomocą [DebuggerBrowsable (DebuggerBrowsableState.Never)] lub [DebuggerDisplay („<załadowany na żądanie>”)]. - Christian Hayter


10
genialne znalezisko! nie jesteś programistą, jesteś prawdziwym debuggerem.
to. __curious_geek

26
Natrafiłem na to nawet najechanie kursorem na zmienną, a nie tylko okno zegarka.
Richard Morgan

31
Udekoruj swoją nieruchomość za pomocą [DebuggerBrowsable(DebuggerBrowsableState.Never)]lub [DebuggerDisplay("<loaded on demand>")].
Christian Hayter,

4
Jeśli opracowujesz klasę frameworka i chcesz mieć funkcję okna podglądu bez zmiany zachowania środowiska wykonawczego leniwie skonstruowanej właściwości, możesz użyć proxy typu debuggera, aby zwrócić wartość, jeśli została już zbudowana, oraz komunikat, że właściwość nie ma został zbudowany, jeśli tak jest. Lazy<T>Klasy (w szczególności w odniesieniu do jego Valuewłasności) jest jednym z przykładów, w którym jest on używany.
Sam Harwell

4
Pamiętam kogoś, kto (z jakiegoś powodu nie mogę pojąć) zmienił wartość obiektu w przeciążeniu ToString. Za każdym razem, gdy się nad nim unosił, podpowiedź dawała mu inną wartość - nie mógł tego pojąć ...
JNF

144

Oto kolejny raz, który mnie dopada:

static void PrintHowLong(DateTime a, DateTime b)
{
    TimeSpan span = a - b;
    Console.WriteLine(span.Seconds);        // WRONG!
    Console.WriteLine(span.TotalSeconds);   // RIGHT!
}

TimeSpan.Seconds to część sekund przedziału czasu (2 minuty i 0 sekund ma wartość 0).

TimeSpan.TotalSeconds to cały przedział czasu mierzony w sekundach (2 minuty mają całkowitą wartość 120).


1
Tak, ten też mnie ma. Myślę, że powinien to być TimeSpan.SecondsPart lub coś takiego, aby wyjaśnić, co reprezentuje.
Dan Diplo,

3
Po ponownym przeczytaniu muszę się zastanawiać, dlaczego w TimeSpanogóle ma jakąś Secondswłasność. Kto w ogóle daje dupie szczura, jaka jest sekunda? Jest to arbitralna wartość zależna od jednostki; Nie wyobrażam sobie żadnego praktycznego zastosowania.
MusiGenesis

2
Ma dla mnie sens, że TimeSpan.TotalSeconds zwróci ... całkowitą liczbę sekund w danym przedziale czasowym.
Ed S.

16
@MusiGenesis właściwość jest przydatna. Co jeśli chcę wyświetlać przedział czasu w podziale na części? Np. Powiedzmy, że twój czas reprezentuje czas trwania „3 godziny 15 minut 10 sekund”. Jak uzyskać dostęp do tych informacji bez właściwości Sekundy, Godziny, Minuty?
SolutionYogi

1
W podobnych interfejsach API użyłem SecondsParti SecondsTotalrozróżniłem oba.
BlueRaja - Danny Pflughoeft

80

Wyciek pamięci, ponieważ nie odblokowałeś zdarzeń.

To nawet złapało niektórych starszych programistów, których znam.

Wyobraź sobie formularz WPF z dużą ilością rzeczy, a gdzieś tam subskrybujesz wydarzenie. Jeśli nie anulujesz subskrypcji, cały formularz zostanie zachowany w pamięci po zamknięciu i usunięciu odwołania.

Wydaje mi się, że problem, który widziałem, polegał na utworzeniu DispatchTimer w formularzu WPF i subskrybowaniu zdarzenia Tick, jeśli nie zrobisz - = na zegarze, twój formularz wycieka pamięć!

W tym przykładzie twój kod porzucenia powinien mieć

timer.Tick -= TimerTickEventHandler;

Ten jest szczególnie trudny, ponieważ utworzyłeś instancję DispatchTimer w formularzu WPF, więc pomyślałbyś, że będzie to wewnętrzne odwołanie obsługiwane przez proces czyszczenia pamięci ... niestety DispatchTimer używa statycznej wewnętrznej listy subskrypcji i usług żądań w wątku interfejsu użytkownika, więc odwołanie jest „własnością” klasy statycznej.


1
Sztuczka polega na tym, aby zawsze zwalniać wszystkie utworzone subskrypcje wydarzeń. Jeśli zaczniesz polegać na tym, że Formularze robią to za Ciebie, możesz być pewien, że nabędziesz nawyku i pewnego dnia zapomnisz wydać wydarzenie gdzieś, gdzie trzeba to zrobić.
Jason Williams

3
Jest tutaj sugestia MS-connect dotycząca słabych zdarzeń referencyjnych , która rozwiązałaby ten problem, chociaż moim zdaniem powinniśmy po prostu całkowicie zastąpić niewiarygodnie słaby model zdarzeń słabo sprzężonym, takim jak ten używany przez CAB.
BlueRaja - Danny Pflughoeft

+1 ode mnie, dziękuję! Cóż, nie, dziękuję za pracę związaną z weryfikacją kodu!
Bob Denny

@ BlueRaja-DannyPflughoeft Przy słabych wydarzeniach masz kolejną gotcha - nie możesz zasubskrybować lambda. Nie możesz pisaćtimer.Tick += (s, e,) => { Console.WriteLine(s); }
Ark-kun

@ Ark-kun tak lambdas sprawiają, że jest jeszcze trudniej, musisz zapisać swoją lambdę do zmiennej i użyć jej w kodzie porzucenia. Trochę niszczy prostotę pisania lambdów, prawda?
Timothy Walters,

63

Może nie jest to naprawdę problem, ponieważ zachowanie jest napisane wyraźnie w MSDN, ale raz złamało mi kark, ponieważ uznałem, że jest to raczej sprzeczne z intuicją:

Image image = System.Drawing.Image.FromFile("nice.pic");

Ten facet pozostawia "nice.pic"plik zablokowany, dopóki obraz nie zostanie usunięty. W tamtym czasie pomyślałem, że fajnie byłoby ładować ikony w locie i nie zdawałem sobie sprawy (na początku), że skończyło mi się z dziesiątkami otwartych i zablokowanych plików! Obraz śledzi, skąd załadował plik ...

Jak to rozwiązać? Myślałem, że jeden liniowiec wykona zadanie. Spodziewałem się dodatkowego parametru FromFile(), ale go nie miałem, więc napisałem to ...

using (Stream fs = new FileStream("nice.pic", FileMode.Open, FileAccess.Read))
{
    image = System.Drawing.Image.FromStream(fs);
}

10
Zgadzam się, że takie zachowanie nie ma sensu. Nie mogę znaleźć innego wytłumaczenia niż „takie zachowanie jest zgodne z projektem”.
MusiGenesis

1
Aha, a co jest świetne w tym obejściu, jeśli spróbujesz wywołać Image.ToStream (nie pamiętam dokładnie tej nazwy) później, to nie zadziała.
Joshua

55
muszę sprawdzić kod. Brb
Esben Skov Pedersen

7
@EsbenSkovPedersen Taki prosty, ale zabawny i suchy komentarz. Zrobił mi dzień.
Inisheer

51

Jeśli liczyć ASP.NET, powiedziałbym, że cykl życia formularzy internetowych jest dla mnie całkiem spory. Spędziłem niezliczone godziny debugując źle napisany kod formularzy internetowych, tylko dlatego, że wielu programistów po prostu nie rozumie, kiedy użyć obsługi zdarzeń (niestety).


26
Właśnie dlatego przeniosłem się do MVC ... bóle głowy związane z
oglądaniem

29
Było jeszcze jedno pytanie poświęcone konkretnie problemom z ASP.NET (zasłużenie). Podstawowa koncepcja ASP.NET (powodująca, że ​​aplikacje internetowe wyglądają jak aplikacje Windows dla programisty) jest tak strasznie wprowadzona w błąd, że nie jestem pewien, czy nawet liczy się jako „gotcha”.
MusiGenesis

1
MusiGenesis Chciałbym móc głosować twój komentarz sto razy.
csauve

3
@MusiGenesis Wydaje się, że jest to obecnie mylące, ale w tamtym czasie ludzie chcieli, aby ich aplikacje internetowe (aplikacje były słowem kluczowym - ASP.NET WebForms nie były tak naprawdę zaprojektowane do obsługi bloga), aby zachowywały się tak samo jak aplikacje Windows. To zmieniło się stosunkowo niedawno i wiele osób wciąż „nie jest tam całkiem”. Cały problem polegał na tym, że abstrakcja była zbyt nieszczelna - sieć nie zachowywała się tak jak aplikacja komputerowa , co doprowadziło do zamieszania u prawie wszystkich.
Luaan

1
Jak na ironię, pierwszą rzeczą, jaką zobaczyłem o ASP.NET, było wideo od Microsoft pokazujące, jak łatwo można stworzyć stronę blogową za pomocą ASP.NET!
MusiGenesis

51

przeciążone == operatory i nieopakowane kontenery (tablice zestawów, zestawy danych itp.):

string my = "my ";
Debug.Assert(my+"string" == "my string"); //true

var a = new ArrayList();
a.Add(my+"string");
a.Add("my string");

// uses ==(object) instead of ==(string)
Debug.Assert(a[1] == "my string"); // true, due to interning magic
Debug.Assert(a[0] == "my string"); // false

Rozwiązania?

  • zawsze używaj, string.Equals(a, b)gdy porównujesz typy ciągów

  • używając generics, List<string>aby upewnić się, że oba operandy są łańcuchami.


6
Masz tam dodatkowe spacje, które sprawiają, że wszystko jest źle - ale jeśli usuniesz spacje, ostatni wiersz nadal będzie prawdziwy, ponieważ „mój” + „ciąg” jest nadal stały.
Jon Skeet

1
ack! masz rację :) ok, trochę edytowałem.
Jimmy

generowane jest ostrzeżenie o takich zastosowaniach.
chakrit

11
Tak, jedną z największych wad języka C # jest operator == w klasie Object. Powinny zmusić nas do korzystania z ReferenceEquals.
erikkallen,

2
Na szczęście od 2.0 mamy leki generyczne. Nie musisz się martwić, jeśli używasz List <ciąg> w powyższym przykładzie zamiast ArrayList. Plus, dzięki temu osiągnęliśmy wydajność! Zawsze usuwam stare odwołania do ArrayLists w naszym starszym kodzie.
JoelC

48
[Serializable]
class Hello
{
    readonly object accountsLock = new object();
}

//Do stuff to deserialize Hello with BinaryFormatter
//and now... accountsLock == null ;)

Morał tej historii: Inicjatory pola nie są uruchamiane podczas deserializacji obiektu


8
Tak, nienawidzę serializacji .NET za brak uruchamiania domyślnego konstruktora. Chciałbym, aby nie było możliwe zbudowanie obiektu bez wywołania jakiegokolwiek konstruktora, ale niestety tak nie jest.
Roman Starkov,

45

DateTime.ToString („dd / MM / rrrr”) ; To nie zawsze da ci dd / MM / rrrr, ale weźmie pod uwagę ustawienia regionalne i zastąpi separator daty w zależności od tego, gdzie jesteś. Więc możesz dostać dd-MM-rrrr lub coś podobnego.

Właściwym sposobem na to jest użycie DateTime.ToString („dd” / „MM” / „yyyy”);


DateTime.ToString („r”) ma konwertować na RFC1123, który używa GMT. GMT znajduje się w ułamku sekundy od UTC, a jednak specyfikator formatu „r” nie konwertuje na UTC , nawet jeśli dana godzina jest określona jako Lokalna.

Powoduje to następującą gotcha (różni się w zależności od odległości czasu lokalnego od UTC):

DateTime.Parse("Tue, 06 Sep 2011 16:35:12 GMT").ToString("r")
>              "Tue, 06 Sep 2011 17:35:12 GMT"

Ups!


19
Zmieniono mm na MM - mm to minuty, a MM to miesiące. Inna gotcha, tak myślę ...
Kobi

1
Widziałem, jak by to był problem, gdybyś tego nie wiedział (nie wiedziałem) ... ale próbuję dowiedzieć się, kiedy chcesz zachować zachowanie, w którym konkretnie próbujesz wydrukować datę, która nie pasuje do twoich ustawień regionalnych.
Beska

6
@Beska: Ponieważ piszesz do pliku, który musi mieć określony format i określony format daty.
GvS,

11
Uważam, że lokalizowane wartości domyślne są gorsze niż na odwrót. Przynajmniej programista całkowicie zignorował lokalizację kod działa na komputerach zlokalizowanych inaczej. W ten sposób kod prawdopodobnie nie działa.
Joshua

32
Właściwie uważam, że właściwym sposobem na to byłobyDateTime.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
BlueRaja - Danny Pflughoeft

44

Widziałem ten opublikowany innego dnia i myślę, że jest dość niejasny i bolesny dla tych, którzy nie wiedzą

int x = 0;
x = x++;
return x;

Jak to zwróci 0, a nie 1, jak większość by się spodziewała


37
Mam nadzieję, że tak naprawdę nie ugryzie ludzi - naprawdę mam nadzieję, że nie napisaliby tego w pierwszej kolejności! (To i tak jest interesujące.)
Jon Skeet

12
Nie sądzę, żeby to było bardzo niejasne ...
Chris Marasti-Georg

10
Przynajmniej w języku C # wyniki są definiowane, jeśli są nieoczekiwane. W C ++ może to być 0 lub 1, lub dowolny inny wynik, w tym zakończenie programu!
James Curran

7
To nie jest gotcha; x = x ++ -> x = x, następnie przyrost x .... x = ++ x -> przyrost x następnie x = x
Kevin

28
@Kevin: Nie sądzę, że to takie proste. Jeśli x = x ++ byłyby równoważne x = x, a następnie x ++, to wynik byłby x = 1. Zamiast tego myślę, że to, co się dzieje, jest oceniane jako pierwsze wyrażenie po prawej stronie znaku równości (daje 0), a następnie x jest inkrementowane (dając x = 1), a na końcu przypisanie jest wykonywane (dając x = 0 jeszcze raz).
Tim Goodman

39

Jestem trochę spóźniony na tę imprezę, ale mam dwie bzdury, które niedawno mnie ugryzły:

Rozdzielczość DateTime

Właściwość Ticksa mierzy czas w 10 milionowych części sekundy (100 nanosekundowych bloków), jednak rozdzielczość nie wynosi 100 nanosekund, to około 15 ms.

Ten kod:

long now = DateTime.Now.Ticks;
for (int i = 0; i < 10; i++)
{
    System.Threading.Thread.Sleep(1);
    Console.WriteLine(DateTime.Now.Ticks - now);
}

da ci wynik (na przykład):

0
0
0
0
0
0
0
156254
156254
156254

Podobnie, jeśli spojrzysz na DateTime.Now.Millisecond, otrzymasz wartości w zaokrąglonych porcjach o wielkości 15.625 ms: 15, 31, 46 itd.

To szczególne zachowanie różni się w zależności od systemu , ale w tym interfejsie API daty / godziny istnieją inne problemy związane z rozdzielczością .


Path.Combine

Świetny sposób na łączenie ścieżek plików, ale nie zawsze działa tak, jak można się spodziewać.

Jeśli drugi parametr zaczyna się od \znaku, nie da ci pełnej ścieżki:

Ten kod:

string prefix1 = "C:\\MyFolder\\MySubFolder";
string prefix2 = "C:\\MyFolder\\MySubFolder\\";
string suffix1 = "log\\";
string suffix2 = "\\log\\";

Console.WriteLine(Path.Combine(prefix1, suffix1));
Console.WriteLine(Path.Combine(prefix1, suffix2));
Console.WriteLine(Path.Combine(prefix2, suffix1));
Console.WriteLine(Path.Combine(prefix2, suffix2));

Daje ci ten wynik:

C:\MyFolder\MySubFolder\log\
\log\
C:\MyFolder\MySubFolder\log\
\log\

17
Kwantyzacja czasów w odstępach ~ 15 ms nie jest spowodowana brakiem dokładności w leżącym u podstaw mechanizmie synchronizacji (zaniedbałem to wcześniej). To dlatego, że Twoja aplikacja działa w systemie wielozadaniowym. System Windows sprawdza twoją aplikację co około 15ms, a podczas krótkiego odcinka czasu aplikacja przetwarza wszystkie wiadomości, które były w kolejce od ostatniego wycinka. Wszystkie połączenia w tym wycinku są zwracane dokładnie w tym samym czasie, ponieważ wszystkie są wykonywane w tym samym czasie.
MusiGenesis

2
@MusiGenesis: Wiem (teraz), jak to działa, ale wydaje mi się, że wprowadzanie tak precyzyjnej miary, która nie jest tak precyzyjna, wydaje mi się mylące. To tak, jakby powiedzieć, że znam swój wzrost w nanometrach, kiedy tak naprawdę zaokrąglam go do najbliższych dziesięciu milionów.
Damovisa,

7
DateTime jest w stanie przechowywać do jednego tika; to DateTime. Teraz nie używa tej dokładności.
Ruben

16
Dodatkowe „\” to gotówka dla wielu osób z Unix / mac / linux. W systemie Windows, jeśli występuje wiodące „\”, oznacza to, że chcemy przejść do katalogu głównego dysku (tj. C :), wypróbuj CDpolecenie, aby zobaczyć, co mam na myśli .... 1) Idź C:\Windows\System322) Wpisz CD \Users3) Woah! Teraz jesteś w C:\Users... GOT IT? ... Path.Combine (@ "C: \ Windows \ System32", @ "\ Users") powinien \Users[current_drive_here]:\Users
zwracać,

8
Nawet bez „snu” działa to w ten sam sposób. Nie ma to nic wspólnego z planowaniem aplikacji co 15 ms. Natywna funkcja wywoływana przez DateTime.UtcNow, GetSystemTimeAsFileTime, wydaje się mieć słabą rozdzielczość.
Jimbo

38

Po uruchomieniu procesu (przy użyciu System.Diagnostics), który zapisuje na konsoli, ale nigdy nie czyta strumienia Console.Out, po określonej ilości danych wyjściowych aplikacja będzie się zawieszać.


3
To samo może się zdarzyć, gdy przekierujesz stdout i stderr i użyjesz dwóch wywołań ReadToEnd w sekwencji. Aby bezpiecznie obsługiwać zarówno stdout, jak i stderr, musisz utworzyć wątek odczytu dla każdego z nich.
Sebastiaan M,

34

Brak skrótów operatora w Linq-To-Sql

Zobacz tutaj .

Krótko mówiąc, wewnątrz warunkowego klauzuli kwerendy LINQ-SQL, nie można używać skrótów warunkowe jak ||i &&aby uniknąć wyjątków odniesienia null; Linq-To-Sql ocenia obie strony operatora OR lub AND, nawet jeśli pierwszy warunek eliminuje potrzebę oceny drugiego warunku!


8
TIL BRB, ponowna optymalizacja kilkuset zapytań LINQ ...
tsilb

30

Używanie parametrów domyślnych z metodami wirtualnymi

abstract class Base
{
    public virtual void foo(string s = "base") { Console.WriteLine("base " + s); }
}

class Derived : Base
{
    public override void foo(string s = "derived") { Console.WriteLine("derived " + s); }
}

...

Base b = new Derived();
b.foo();

Wyjście:
podstawa pochodna


10
Dziwne, myślałem, że to całkowicie oczywiste. Jeśli deklarowany typ to Base, skąd kompilator powinien uzyskać wartość domyślną, jeśli nie Base? Pomyślałbym, że nieco bardziej gotcha jest, że domyślna wartość może być inna, jeśli deklarowany typ jest typem pochodnym , mimo że metoda nazywana (statyczna) jest metodą podstawową.
Timwi

1
dlaczego jedna implementacja metody miałaby otrzymać wartość domyślną innej implementacji?
staafl

1
@staafl Domyślne argumenty są rozwiązywane w czasie kompilacji, a nie w czasie wykonywania.
fredoverflow

1
Powiedziałbym, że ta gotcha to parametry domyślne w ogóle - ludzie często nie zdają sobie sprawy, że są rozwiązywani w czasie kompilacji, a nie w czasie wykonywania.
Luaan

4
@FredOverflow, moje pytanie było koncepcyjne. Chociaż zachowanie ma sens w stosunku do implementacji, jest nieintuicyjne i prawdopodobnie źródłem błędów. IMHO kompilator C # nie powinien umożliwiać zmiany domyślnych wartości parametrów podczas nadpisywania.
staafl

27

Wartościuj obiekty w kolekcjach zmiennych

struct Point { ... }
List<Point> mypoints = ...;

mypoints[i].x = 10;

nie ma wpływu.

mypoints[i]zwraca kopię Pointobiektu wartości. C # szczęśliwie pozwala modyfikować pole kopii. Po cichu nic nie robiąc.


Aktualizacja: Wydaje się, że zostało to naprawione w C # 3.0:

Cannot modify the return value of 'System.Collections.Generic.List<Foo>.this[int]' because it is not a variable

6
Rozumiem, dlaczego jest to mylące, biorąc pod uwagę, że rzeczywiście działa z tablicami (w przeciwieństwie do twojej odpowiedzi), ale nie z innymi dynamicznymi kolekcjami, takimi jak List <Point>.
Lasse V. Karlsen

2
Masz rację. Dzięki. Naprawiłem swoją odpowiedź :). arr[i].attr=jest specjalną składnią dla tablic, których nie można kodować w kontenerach bibliotecznych; (. Dlaczego (<wyrażenie wartości>). attr = <expr> jest w ogóle dozwolone? Czy to może kiedykolwiek mieć sens?
Bjarke Ebert

1
@Bjarke Ebert: W niektórych przypadkach miałoby to sens, ale niestety nie ma sposobu, aby kompilator mógł je zidentyfikować i zezwolić. Przykładowy scenariusz użycia: niezmienna konstrukcja, która zawiera odniesienie do kwadratowej dwuwymiarowej tablicy wraz ze wskaźnikiem „obróć / odwróć”. Sama struktura byłaby niezmienna, więc zapisywanie w elemencie instancji tylko do odczytu powinno być w porządku, ale kompilator nie będzie wiedział, że ustawiający właściwość tak naprawdę nie napisze struktury, a zatem nie pozwoli na to .
supercat

25

Być może nie najgorsze, ale niektóre części .NET używają stopni, podczas gdy inne używają radianów (a dokumentacja, która pojawia się z Intellisense nigdy nie mówi ci, które, musisz odwiedzić MSDN, aby się dowiedzieć)

Można tego wszystkiego uniknąć, Anglezamiast tego mając klasę ...


Dziwi mnie, że otrzymałem tak wiele pozytywnych opinii, biorąc pod uwagę, że moje inne problemy są znacznie gorsze niż to
BlueRaja - Danny Pflughoeft,

22

Dla programistów C / C ++ przejście na C # jest naturalne. Jednak największa gotcha, na którą natknąłem się osobiście (i widziałem, jak inni dokonują tego samego przejścia), nie do końca rozumie różnicę między klasami i strukturami w języku C #.

W C ++ klasy i struktury są identyczne; różnią się jedynie domyślną widocznością, gdzie klasy domyślnie są widoczne dla prywatności, a struktury domyślnie - dla widoczności publicznej. W C ++ ta definicja klasy

    class A
    {
    public:
        int i;
    };

jest funkcjonalnie równoważny tej definicji struktury.

    struct A
    {
        int i;
    };

Jednak w języku C # klasy są typami referencyjnymi, a struktury są typami wartości. To sprawia, że DUŻA różnica w (1) podejmowaniu decyzji, kiedy użyć jednego nad drugim, (2) testowaniu równości obiektu, (3) wydajności (np. Boksowaniu / rozpakowywaniu) itp.

W sieci dostępne są wszelkiego rodzaju informacje związane z różnicami między nimi (np. Tutaj ). Gorąco zachęcam każdego, kto przejdzie do C #, aby przynajmniej miał praktyczną wiedzę na temat różnic i ich konsekwencji.


13
Więc najgorsze, że ludzie nie zadają sobie trudu, aby nauczyć się języka przed jego użyciem?
BlueRaja - Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft Bardziej jak klasyczna gotcha z pozornie podobnych języków - używają bardzo podobnych słów kluczowych i w wielu przypadkach składni, ale działają na wiele innych sposobów.
Luaan

19

Odśmiecanie i usuwanie (). Chociaż nie musisz nic robić, aby zwolnić pamięć , nadal musisz zwolnić zasoby za pomocą Dispose (). Jest to niezwykle łatwa rzecz do zapomnienia, gdy używasz WinForms lub śledzisz obiekty w jakikolwiek sposób.


2
Blok using () starannie rozwiązuje ten problem. Za każdym razem, gdy zobaczysz wezwanie do usunięcia, możesz natychmiast i bezpiecznie refaktoryzować użycie za pomocą ().
Jeremy Frey

5
Myślę, że problemem było prawidłowe wdrożenie IDisposable.
Mark Brackett,

4
Z drugiej strony nawyk using () może cię nieoczekiwanie ugryźć, na przykład podczas pracy z PInvoke. Nie chcesz pozbywać się czegoś, do czego API wciąż się odwołuje.
MusiGenesis

3
Prawidłowa implementacja IDisposable jest bardzo trudna do zrozumienia i nawet najlepsza rada, którą znalazłem na ten temat (Wytyczne .NET Framework) może być myląca w stosowaniu, dopóki w końcu jej nie „dostaniesz”.
Quibblesome

1
Najlepsza rada, jaką kiedykolwiek znalazłem na temat IDisposable, pochodzi od Stephena Cleary'ego, w tym trzy proste zasady i szczegółowy artykuł na temat IDisposable
Roman Starkov 10'11

19

Implementacja tablic IList

Ale nie wdrażaj go. Gdy zadzwonisz do Add, powiesz, że to nie działa. Dlaczego więc klasa implementuje interfejs, gdy nie może go obsługiwać?

Kompiluje, ale nie działa:

IList<int> myList = new int[] { 1, 2, 4 };
myList.Add(5);

Często mamy ten problem, ponieważ serializator (WCF) zamienia wszystkie ILists w tablice i otrzymujemy błędy czasu wykonywania.


8
IMHO, problem polega na tym, że Microsoft nie ma wystarczającej liczby interfejsów zdefiniowanych dla kolekcji. IMHO, powinien mieć iEnumerable, iMultipassEnumerable (obsługuje reset i gwarantuje, że wiele przebiegów będzie pasować), iLiveEnumerable (miałby częściowo zdefiniowaną semantykę, jeśli kolekcja zmienia się podczas wyliczania - zmiany mogą lub nie mogą pojawiać się w wyliczaniu, ale nie powinny powodować fałszywe wyniki lub wyjątki), iReadIndexable, iReadWriteIndexable itp. Ponieważ interfejsy mogą „dziedziczyć” inne interfejsy, nie wymagałoby to dużo dodatkowej pracy, jeśli w ogóle (zapisałoby to kody pośrednie NotImplemented).
supercat,

@ supercat, byłoby to mylące jak diabli dla początkujących i niektórych programistów od dawna. Myślę, że kolekcje .NET i ich interfejsy są cudownie eleganckie. Ale doceniam twoją pokorę. ;)
Jordan

@Jordan: Od czasu napisania powyższego zdecydowałem, że lepszym podejściem byłoby posiadanie obu IEnumerable<T>i IEnumerator<T>obsługa Featureswłaściwości, a także niektórych „opcjonalnych” metod, których użyteczność byłaby określona przez to, co zgłosiły „Funkcje”. Podtrzymuję jednak moją główną uwagę, a mianowicie to, że w niektórych przypadkach odbieranie kodu IEnumerable<T>będzie wymagało silniejszych obietnic niż IEnumerable<T>zapewnia. Dzwonienie ToListprzyniosłoby IEnumerable<T>taką obietnicę, ale w wielu przypadkach byłoby niepotrzebnie kosztowne. Sądzę, że powinno być ...
supercat

... sposób, za pomocą którego kod odbierający IEnumerable<T>może w razie potrzeby wykonać kopię treści , ale może powstrzymać się od niepotrzebnego działania.
supercat

Twoja opcja jest absolutnie nieczytelna. Kiedy widzę IList w kodzie, wiem, z czym pracuję, zamiast sondować właściwość Features. Programiści lubią zapominać, że ważną cechą kodu jest to, że może on być czytany przez ludzi, a nie tylko komputery. Przestrzeń nazw kolekcji .NET nie jest idealna, ale jest dobra, a czasem znalezienie najlepszego rozwiązania nie jest kwestią idealnego dopasowania zasady. Jednym z najgorszych kodów, z którymi pracowałem, był kod, który idealnie pasował do DRY. Zeskrobałem go i przepisałem. To był po prostu zły kod. W ogóle nie chciałbym używać twojego frameworka.
Jordan

18

zakres zmiennych pętli foreach!

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    l.Add(() => s);
}

foreach (var a in l)
    Console.WriteLine(a());

wypisuje pięć „amet”, podczas gdy poniższy przykład działa dobrze

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    var t = s;
    l.Add(() => t);
}

foreach (var a in l)
    Console.WriteLine(a());

11
Jest to zasadniczo równoważne przykładowi Jona z anonimowymi metodami.
Mehrdad Afshari

3
Zauważ, że jest to jeszcze bardziej mylące z foreach, gdzie zmienna „s” jest łatwiejsza do zmieszania ze zmienną o zasięgu. W przypadku wspólnych pętli for zmienna indeksu jest wyraźnie taka sama dla każdej iteracji.
Mikko Rantanen,

2
blogs.msdn.com/ericlippert/archive/2009/11/12/... i tak, szkoda, że ​​zakres zmiennej nie był „poprawnie”.
Roman Starkov,

2
Zostało to naprawione w C # 5 .
Johnbot

Zasadniczo drukujesz tę samą zmienną bez zmiany.
Jordan

18

MS SQL Server nie może obsłużyć dat sprzed 1753 r. Co istotne, nie jest zsynchronizowany ze DateTime.MinDatestałą .NET , która wynosi 1/1/1. Więc jeśli spróbujesz uratować umysł, zniekształconą datę (jak ostatnio przydarzyło mi się to podczas importu danych) lub po prostu datę urodzenia Wilhelma Zdobywcy, będziesz miał kłopoty. Nie ma wbudowanego obejścia tego problemu; jeśli prawdopodobnie będziesz musiał pracować z datami sprzed 1753 r., musisz napisać własne obejście.


17
Szczerze mówiąc, myślę, że MS SQL Server ma to prawo i .Net się myli. Jeśli przeprowadzasz badania, wiesz, że daty sprzed 1751 roku stają się funky z powodu zmian kalendarza, dni całkowicie pomijanych itp. Większość RDBM ma jakiś punkt odcięcia. To powinno dać ci punkt wyjścia: ancestry.com/learn/library/article.aspx?article=3358
NotMe

11
Poza tym datą jest 1753 .. To był pierwszy raz, kiedy mamy ciągły kalendarz bez pomijania dat. SQL 2008 wprowadził typ danych Data i datetime2, który może akceptować daty od 1/1/01 do 12/31/9999. Jednak porównania dat przy użyciu tych typów powinny być podejrzane, jeśli naprawdę porównujesz daty sprzed 1753 roku.
NotMe

Och, racja, 1753, poprawione, dzięki.
Shaul Behr

Czy naprawdę warto porównywać daty z takimi datami? To znaczy, dla History Channel ma to wiele sensu, ale nie widzę siebie, który chciałbym poznać dokładny dzień tygodnia, w którym odkryta została Ameryka.
Camilo Martin

5
Za pośrednictwem Wikipedii w Dzień Juliański można znaleźć 13-liniowy podstawowy program CALJD.BAS opublikowany w 1984 r., Który może wykonywać obliczenia dat od około 5000 lat pne, biorąc pod uwagę dni przestępne i dni pominięte w 1753 r. Więc nie rozumiem, dlaczego „nowoczesny „systemy takie jak SQL2008 powinny działać gorzej. Być może nie jesteś zainteresowany poprawną reprezentacją daty w XV wieku, ale inni mogą, a nasze oprogramowanie powinno sobie z tym poradzić bez błędów. Kolejnym problemem są sekundy przestępne. . .
Roland

18

Nasty Linq Caching Gotcha

Zobacz moje pytanie, które doprowadziło do tego odkrycia, oraz blogera, który odkrył problem.

W skrócie, DataContext utrzymuje pamięć podręczną wszystkich obiektów Linq-do-Sql, które kiedykolwiek załadowałeś. Jeśli ktokolwiek dokona jakichkolwiek zmian w rekordzie, który wcześniej załadowałeś, nie będziesz w stanie uzyskać najnowszych danych, nawet jeśli jawnie ponownie go załadujesz!

Wynika to z właściwości wywoływanej ObjectTrackingEnabledw obiekcie DataContext, która domyślnie ma wartość true. Jeśli ustawisz tę właściwość na false, rekord będzie ładowany od nowa za każdym razem ... ALE ... nie możesz zachować żadnych zmian w tym rekordzie za pomocą SubmitChanges ().

GOTCHA!


Iv właśnie spędził półtora dnia (i mnóstwo włosów!) Ścigając ten BŁĄD ...
Surgical Coder

Nazywa się to konfliktem współbieżności i nadal jest gotcha, chociaż istnieją pewne sposoby na obejście tego teraz, choć są one zwykle trudne. DataContext był koszmarem. O_o
Jordan

17

Kontrakt na Stream.Read jest czymś, co widziałem, że podróżuje do wielu osób:

// Read 8 bytes and turn them into a ulong
byte[] data = new byte[8];
stream.Read(data, 0, 8); // <-- WRONG!
ulong data = BitConverter.ToUInt64(data);

Powodem tego jest to, że Stream.Readodczytuje co najwyżej określoną liczbę bajtów, ale może całkowicie odczytać tylko 1 bajt, nawet jeśli kolejne 7 bajtów jest dostępnych przed końcem strumienia.

To nie pomaga, że to wygląda tak podobne do Stream.Write, które jest gwarantowane napisałem wszystkie bajty jeśli zwróci bez wyjątku. Nie pomaga również to, że powyższy kod działa prawie cały czas . I oczywiście nie pomaga to w tym, że nie ma gotowej, wygodnej metody prawidłowego odczytu dokładnie N bajtów.

Tak więc, aby zatkać dziurę i zwiększyć świadomość tego, oto przykład prawidłowego sposobu:

    /// <summary>
    /// Attempts to fill the buffer with the specified number of bytes from the
    /// stream. If there are fewer bytes left in the stream than requested then
    /// all available bytes will be read into the buffer.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="buffer">Buffer to write the bytes to.</param>
    /// <param name="offset">Offset at which to write the first byte read from
    ///                      the stream.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    /// <returns>Number of bytes read from the stream into buffer. This may be
    ///          less than requested, but only if the stream ended before the
    ///          required number of bytes were read.</returns>
    public static int FillBuffer(this Stream stream,
                                 byte[] buffer, int offset, int length)
    {
        int totalRead = 0;
        while (length > 0)
        {
            var read = stream.Read(buffer, offset, length);
            if (read == 0)
                return totalRead;
            offset += read;
            length -= read;
            totalRead += read;
        }
        return totalRead;
    }

    /// <summary>
    /// Attempts to read the specified number of bytes from the stream. If
    /// there are fewer bytes left before the end of the stream, a shorter
    /// (possibly empty) array is returned.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    public static byte[] Read(this Stream stream, int length)
    {
        byte[] buf = new byte[length];
        int read = stream.FillBuffer(buf, 0, length);
        if (read < length)
            Array.Resize(ref buf, read);
        return buf;
    }

1
Lub, w wyraźnej np var r = new BinaryReader(stream); ulong data = r.ReadUInt64();. BinaryReader też ma FillBuffermetodę ...
jimbobmcgee

15

Wydarzenia

Nigdy nie zrozumiałem, dlaczego wydarzenia są cechą językową. Są skomplikowane w użyciu: musisz sprawdzić, czy nie ma wartości zerowej przed dzwonieniem, musisz się wyrejestrować (sam), nie możesz dowiedzieć się, kto jest zarejestrowany (np. Czy się zarejestrowałem?). Dlaczego wydarzenie nie jest klasą w bibliotece? Zasadniczo specjalista List<delegate>?


1
Również wielowątkowość jest bolesna. Wszystkie te problemy, z wyjątkiem zerowej rzeczy, zostały naprawione w CAB (którego funkcje powinny być po prostu wbudowane w język) - zdarzenia są deklarowane globalnie, a każda metoda może zadeklarować się jako „subskrybent” dowolnego zdarzenia. Moim jedynym problemem z CAB jest to, że globalne nazwy zdarzeń są ciągami, a nie wyliczeniami (które mogłyby zostać naprawione przez bardziej inteligentne wyliczenia, takie jak Java, które z natury działają jako ciągi!) . CAB jest trudny w konfiguracji, ale nie jest to proste klonem open source dostępne tutaj .
BlueRaja - Danny Pflughoeft

3
Nie podoba mi się realizacja zdarzeń .net. Subskrypcja zdarzeń powinna być obsługiwana przez wywołanie metody, która dodaje subskrypcję i zwraca identyfikator IDisposable, który po usunięciu usunie subskrypcję. Nie ma potrzeby specjalnej konstrukcji łączącej metodę „dodaj” i „usuń”, której semantyka może być nieco niejasna, zwłaszcza jeśli ktoś spróbuje dodać, a następnie usunąć delegata multiemisji (np. Dodaj „B”, a następnie „AB”, a następnie usuń „B” (pozostawiając „BA”) i „AB” (wciąż pozostawiając „BA”). Ups
supercat

@ superupat Jak przepisałbyś button.Click += (s, e) => { Console.WriteLine(s); }?
Ark-kun,

Gdybym musiał móc zrezygnować z subskrypcji oddzielnie od innych wydarzeń IEventSubscription clickSubscription = button.SubscribeClick((s,e)=>{Console.WriteLine(s);});i zrezygnować z subskrypcji za pośrednictwem clickSubscription.Dispose();. Gdyby mój obiekt zachował wszystkie subskrypcje przez cały okres użytkowania, MySubscriptions.Add(button.SubscribeClick((s,e)=>{Console.WriteLine(s);}));a następnie MySubscriptions.Dispose()zabił wszystkie subskrypcje.
supercat

@ Ark-kun: Konieczność utrzymywania obiektów zamykających subskrypcje zewnętrzne może wydawać się uciążliwa, ale uznanie subskrypcji za byty umożliwiłoby ich agregację z typem, który zapewni, że wszystkie zostaną wyczyszczone, co w innym przypadku byłoby bardzo trudne.
supercat

14

Dzisiaj naprawiłem błąd, który wymknął się przez długi czas. Błąd występował w klasie ogólnej, która była używana w scenariuszu wielowątkowym, a do zapewnienia synchronizacji bez blokady za pomocą Interlocked zastosowano statyczne pole int. Błąd został spowodowany, ponieważ każda instancja klasy ogólnej dla typu ma własną statyczność. Więc każdy wątek ma swoje własne pole statyczne i nie był używany zamek zgodnie z przeznaczeniem.

class SomeGeneric<T>
{
    public static int i = 0;
}

class Test
{
    public static void main(string[] args)
    {
        SomeGeneric<int>.i = 5;
        SomeGeneric<string>.i = 10;
        Console.WriteLine(SomeGeneric<int>.i);
        Console.WriteLine(SomeGeneric<string>.i);
        Console.WriteLine(SomeGeneric<int>.i);
    }
}

Drukuje 5 10 5


5
możesz mieć nie-ogólną klasę podstawową, która definiuje statykę i dziedziczy po niej ogólne. Chociaż nigdy nie zakochałem się w tym zachowaniu w C # - wciąż pamiętam długie godziny debugowania niektórych szablonów C ++ ... Eww! :)
Paulius

7
Dziwne, myślałem, że to oczywiste. Pomyśl tylko, co powinien zrobić, gdyby imiał taki typ T.
Timwi

1
Parametr typu jest częścią parametru Type. SomeGeneric<int>jest innym typem SomeGeneric<string>; więc oczywiście każdy ma swójpublic static int i
radarbob

13

Wyliczenia można oceniać więcej niż raz

Ugryzie cię, gdy będziesz miał leniwie wyliczony i powtórzysz go dwa razy i uzyskasz inne wyniki. (lub otrzymujesz te same wyniki, ale wykonuje się niepotrzebnie dwa razy)

Na przykład podczas pisania pewnego testu potrzebowałem kilku plików tymczasowych do przetestowania logiki:

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName());

foreach (var file in files)
    File.WriteAllText(file, "HELLO WORLD!");

/* ... many lines of codes later ... */

foreach (var file in files)
    File.Delete(file);

Wyobraź sobie moją niespodziankę, gdy File.Delete(file)rzuca FileNotFound!!

To, co się tutaj dzieje, polega na tym, że fileswyliczalność została powtórzona dwukrotnie (wyniki z pierwszej iteracji po prostu nie są zapamiętywane), a przy każdej nowej iteracji będziesz ponownie wywoływał, Path.GetTempFilename()aby uzyskać inny zestaw nazw plików tymczasowych.

Rozwiązaniem jest oczywiście wyliczenie wartości za pomocą ToArray()lub ToList():

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName())
    .ToArray();

Jest to jeszcze bardziej przerażające, gdy robisz coś wielowątkowego, na przykład:

foreach (var file in files)
    content = content + File.ReadAllText(file);

i okazuje się, że content.Lengthwciąż jest 0 po wszystkich zapisach !! Następnie zaczynasz rygorystycznie sprawdzać, czy nie masz warunków do wyścigu, gdy ... po jednej straconej godzinie ... zorientowałeś się, że to po prostu ta mała, wyliczalna rzecz, którą zapomniałeś ...


To jest z założenia. Nazywa się to odroczeniem wykonania. Między innymi ma symulować konstrukcje TSQL. Za każdym razem, gdy wybierasz z widoku sql, otrzymujesz inne wyniki. Pozwala także na tworzenie łańcuchów, które są pomocne w przypadku zdalnych magazynów danych, takich jak SQL Server. W przeciwnym razie x.Select.Where.OrderBy wysłałby 3 osobne polecenia do bazy danych ...
as9876 14.07.15

@AYS, czy brakuje Ci słowa „Gotcha” w tytule pytania?
chakrit

Myślałem, że gotcha oznacza nadzór nad projektantami, a nie coś celowego.
as9876

Może powinien istnieć inny typ nieodnawialnych elementów IEnumerable. Jak AutoBufferedEnumerable? Można to łatwo wdrożyć. Wydaje się, że jest to spowodowane głównie brakiem wiedzy programisty. Nie sądzę, aby coś było nie tak z obecnym zachowaniem.
Eldritch Conundrum

13

Właśnie znalazłem dziwny, w którym utknąłem na chwilę w debugowaniu:

Możesz zwiększyć wartość null dla wartości zerowej int bez rzucania wykroczenia, a wartość pozostanie null.

int? i = null;
i++; // I would have expected an exception but runs fine and stays as null

Jest to wynik tego, w jaki sposób C # wykorzystuje operacje dla typów zerowalnych. Jest trochę podobny do NaN zużywającego wszystko, co do niego wrzucisz.
IllidanS4 chce Moniki z powrotem

10
TextInfo textInfo = Thread.CurrentThread.CurrentCulture.TextInfo;

textInfo.ToTitleCase("hello world!"); //Returns "Hello World!"
textInfo.ToTitleCase("hElLo WoRld!"); //Returns "Hello World!"
textInfo.ToTitleCase("Hello World!"); //Returns "Hello World!"
textInfo.ToTitleCase("HELLO WORLD!"); //Returns "HELLO WORLD!"

Tak, to zachowanie jest udokumentowane, ale z pewnością nie jest to właściwe.


5
Nie zgadzam się - kiedy słowo jest pisane wielkimi literami, może mieć specjalne znaczenie, że nie chcesz zepsuć tytułu Case, np. „Prezydent Stanów Zjednoczonych” -> „prezydent Stanów Zjednoczonych”, a nie „prezydent USA".
Shaul Behr

5
@Shaul: W takim przypadku powinni podać to jako parametr, aby uniknąć nieporozumień, ponieważ nigdy nie spotkałem nikogo, kto spodziewałby się takiego zachowania z wyprzedzeniem - co czyni to gotcha !
BlueRaja - Danny Pflughoeft
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.