Что такое худшая ошибка в C # или .NET? [закрыто]


377

Я недавно работал с DateTimeобъектом и написал что-то вроде этого:

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

В документации по intellisense AddDays()говорится, что она добавляет день к дате, а это не так - фактически она возвращает дату с добавленным днем, поэтому вы должны написать ее следующим образом:

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

Этот укусил меня несколько раз прежде, поэтому я подумал, что было бы полезно занести в каталог худшие ошибки C #.


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

23
AFAIK, все встроенные типы значений являются неизменяемыми, по крайней мере в том смысле, что любой метод, включенный в тип, возвращает новый элемент, а не изменяет существующий элемент. По крайней мере, я не могу думать о том, что у меня на макушке не получается: все красиво и последовательно.
Джоэл Коухорн

6
Тип изменяемого значения: System.Collections.Generics.List.Enumerator :( (И да, вы можете видеть, как он ведет себя странно, если вы попытаетесь достаточно усердно.)
Джон Скит,

13
Intellisense дает вам всю необходимую информацию. Он говорит, что возвращает объект DateTime. Если бы он просто изменил тот, который вы передали, это был бы пустой метод.
Джон Крафт

20
Не обязательно: StringBuilder.Append (...) возвращает «this», например. Это довольно часто встречается в беглых интерфейсах.
Джон Скит

Ответы:


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

Blammo. Ваше приложение падает без трассировки стека. Бывает все время.

(Обратите внимание на заглавные MyVarбуквы вместо строчных myVarв получателе.)


112
и так подходит для этого сайта :)
gbjbaanb

62
Я ставлю подчеркивание на частном члене, очень помогает!
чакрит

61
Я использую автоматические свойства там, где могу,
часто решает

28
Это БОЛЬШАЯ причина использовать префиксы для ваших личных полей (есть и другие, но это хорошая): _myVar, m_myVar
jrista

205
@jrista: О, пожалуйста, НЕТ ... не м_ ... ах, ужас ...
fretje

254

Type.GetType

Тот, кого я видел, кусает многих людей Type.GetType(string). Они задаются вопросом, почему это работает для типов в их собственной сборке, и некоторым типам нравится System.String, но нет System.Windows.Forms.Form. Ответ в том, что он выглядит только в текущей сборке и в mscorlib.


Анонимные методы

C # 2.0 ввел анонимные методы, что привело к неприятным ситуациям, подобным этому:

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();
        }
    }
}

Что это распечатает? Ну, это полностью зависит от планирования. Он напечатает 10 чисел, но, вероятно, не будет печатать 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, что вы можете ожидать. Проблема в том, что это iзахваченная переменная, а не ее значение в момент создания делегата. Это может быть легко решено с помощью дополнительной локальной переменной правильной области видимости:

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();
        }
    }
}

Отложенное выполнение блоков итераторов

Этот «модульный тест бедняка» не проходит - почему бы и нет?

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
        }
    }
}

Ответ заключается в том, что код в исходном CapitalLettersкоде не выполняется до тех пор, пока метод итератора не MoveNext()будет впервые вызван.

У меня есть некоторые другие странности на моей странице мозгов .


25
Пример итератора обманчив!
Джимми

8
почему бы не разделить это на 3 ответа, чтобы мы могли голосовать каждый вместо всех вместе?
Чакрит

13
@chakrit: Оглядываясь назад, это, вероятно, было бы хорошей идеей, но я думаю, что сейчас слишком поздно. Возможно, это выглядело так, как будто я просто пытался получить больше представителей ...
Джон Скит,

19
На самом деле Type.GetType работает, если вы предоставите AssemblyQualifiedName. Type.GetType ("System.ServiceModel.EndpointNotFoundException, System.ServiceModel, версия = 3.0.0.0, культура = нейтральная, PublicKeyToken = b77a5c561934e089");
Chilltemp

2
@kentaromiura: разрешение перегрузки начинается с самого производного типа и работает с деревом, но только с учетом методов, первоначально объявленных в типе, на который оно смотрит. Foo (int) переопределяет базовый метод, поэтому не рассматривается. Foo (объект) применим, поэтому разрешение перегрузки на этом останавливается. Странно, я знаю.
Джон Скит

194

Перебрасывание исключений

Гоча, которая получает много новых разработчиков, является семантикой исключения повторного выброса.

Много времени я вижу код, подобный следующему

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

Проблема в том, что он стирает трассировку стека и значительно усложняет диагностику, потому что вы не можете отследить, откуда возникло исключение.

Правильный код - это либо оператор throw без аргументов:

catch(Exception)
{
    throw;
}

Или обернуть исключение в другое и использовать внутреннее исключение, чтобы получить исходную трассировку стека:

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

К счастью, меня кто-то научил об этом на моей первой неделе и нашел в коде более старших разработчиков. Есть: catch () {throw; } Так же, как второй фрагмент кода? catch (исключение e) {throw; } только он не создает объект Exception и не заполняет его?
StuperUser

Помимо ошибки использования throw ex (или throw e) вместо просто throw, я должен задаться вопросом, какие бывают случаи, когда стоит отловить исключение только для того, чтобы снова сгенерировать его.
Райан Ланди

13
@Kyralessa: есть много случаев: например, если вы хотите откатить транзакцию, прежде чем вызывающая сторона получит исключение. Вы откатываете, а затем отбрасываете.
Р. Мартиньо Фернандес

7
Я вижу это все время, когда люди перехватывают и перебрасывают исключения только потому, что их учат, что они должны перехватывать все исключения, не осознавая, что они будут перехвачены в стеке вызовов. Это сводит меня с ума.
Джеймс Вестгейт

5
@Kyralessa самый большой случай, когда вы должны заняться регистрацией. Записать ошибку в catch и перебросить ..
nawfal

194

Окно часов Гейзенберга

Это может сильно укусить вас, если вы делаете вещи по требованию, например так:

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

Теперь предположим, что у вас есть код где-то еще, использующий это:

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

Теперь вы хотите отладить свой CreateMyObj()метод. Таким образом, вы ставите точку останова в строке 3 выше с намерением войти в код. На всякий случай вы также ставите точку останова над строкой с надписью _myObj = CreateMyObj();и даже точку останова внутри CreateMyObj()себя.

Код достигает вашей точки останова в строке 3. Вы входите в код. Вы ожидаете ввести условный код, потому что _myObjон явно равен нулю, верно? Э-э ... так ... почему оно пропустило условие и сразу пошло return _myObj?! Вы наводите указатель мыши на _myObj ... и это действительно имеет значение! Как это произошло?!

Ответ в том, что ваша IDE заставила его получить значение, потому что у вас открыто окно «watch» - особенно окно «Autos», которое отображает значения всех переменных / свойств, относящихся к текущей или предыдущей строке выполнения. Когда вы достигли своей точки останова в строке 3, окно наблюдения решило, что вам будет интересно узнать значение MyObj- так что за кулисами, игнорируя любую из ваших точек останова , оно пошло и вычислило значение MyObjдля вас - включая вызов CreateMyObj()этого устанавливает значение _myObj!

Вот почему я называю это Окно часов Гейзенберга - вы не можете наблюдать значение, не влияя на него ... :)

ПОПАЛСЯ!


Редактировать - я считаю, что комментарий @ ChristianHayter заслуживает включения в основной ответ, потому что он выглядит как эффективный обходной путь для этой проблемы. Так что в любое время у вас есть лениво загруженное свойство ...

Украсьте свое свойство с помощью [DebuggerBrowsable (DebuggerBrowsableState.Never)] или [DebuggerDisplay («<загружено по требованию>»)]. - Кристиан Хейтер


10
блестящая находка! ты не программист, ты настоящий отладчик.
это __curious_geek

26
Я сталкивался с этим даже при наведении на переменную, а не только на окно наблюдения.
Ричард Морган

31
Украсьте свою собственность с помощью [DebuggerBrowsable(DebuggerBrowsableState.Never)]или [DebuggerDisplay("<loaded on demand>")].
Кристиан Хейтер,

4
Если вы разрабатываете каркасный класс и хотите, чтобы функциональность окна наблюдения не изменяла поведение среды выполнения лениво сконструированного свойства, вы можете использовать прокси-сервер типа отладчика, чтобы вернуть значение, если оно уже создано, и сообщение о том, что свойство не имеет был построен, если это так. Lazy<T>Класс (в частности , для его Valueсобственности) является одним из примеров , когда это используется.
Сэм Харуэлл,

4
Я вспоминаю кого-то, кто (по какой-то причине я не могу понять) изменил значение объекта в результате перегрузки ToString. Каждый раз, когда он нависал над ним, подсказка давала ему другое значение - он не мог понять это ...
JNF

144

Вот еще один раз, который получает меня:

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

TimeSpan.Seconds - это секундная часть временного промежутка (значение 2 секунды и 0 секунд равно 0).

TimeSpan.TotalSeconds - это полный промежуток времени, измеренный в секундах (общее время в 2 минуты равно 120).


1
Да, этот тоже меня достал. Я думаю, что это должен быть TimeSpan.SecondsPart или что-то еще, чтобы было более понятно, что он представляет.
Дэн

3
На Перечитывая это, я должен задаться вопросом , почему TimeSpanдаже имеет в Secondsсобственности вообще. В любом случае, кто дает крысиной заднице, что такое секундная доля временного интервала? Это произвольное, зависящее от единицы значение; Я не могу представить себе никакого практического использования для этого.
MusiGenesis

2
Имеет смысл для меня, что TimeSpan.TotalSeconds вернет ... общее количество секунд в промежутке времени.
Эд С.

16
@MusiGenesis это свойство полезно. Что делать, если я хочу отобразить временную шкалу, разбитую на части? Например, допустим, ваш интервал времени представляет собой продолжительность «3 часа 15 минут 10 секунд». Как вы можете получить доступ к этой информации без свойств Секунд, Часов, Минут?
SolutionYogi

1
В аналогичных API я использовал SecondsPartи SecondsTotalразличать два.
BlueRaja - Дэнни Пфлугхофт

80

Утечка памяти, потому что вы не отцепили события.

Это даже поймало некоторых старших разработчиков, которых я знаю.

Представьте себе форму WPF с множеством вещей, и где-то там вы подписываетесь на событие. Если вы не отмените подписку, тогда вся форма будет храниться в памяти после закрытия и разыменования.

Я полагаю, что проблема, которую я видел, заключалась в создании DispatchTimer в форме WPF и подписке на событие Tick, если вы не сделаете - = в таймере, ваша форма утечка памяти!

В этом примере ваш код разрыва должен иметь

timer.Tick -= TimerTickEventHandler;

Это особенно сложно, поскольку вы создали экземпляр DispatchTimer внутри формы WPF, поэтому вы могли бы подумать, что это будет внутренняя ссылка, обрабатываемая процессом сборки мусора ... к сожалению, DispatchTimer использует статический внутренний список подписок и служб запросы в потоке пользовательского интерфейса, поэтому ссылка «принадлежит» статическому классу.


1
Хитрость в том, чтобы всегда освобождать все подписки на события, которые вы создаете. Если вы начнете полагаться на то, что формы делают это за вас, вы можете быть уверены, что приобретете привычку и однажды забудете выпустить событие где-то, где это нужно сделать.
Джейсон Уильямс

3
Существует МС-Коннект предложение для слабых референтных событий здесь , которые бы решить эту проблему, хотя на мой взгляд , мы просто должны полностью заменить невероятно плохую модель событий с слабосвязанных один, как это используется САВ.
BlueRaja - Дэнни Пфлугхофт

+1 от меня, спасибо! Ну, нет, спасибо за работу по проверке кода, которую я должен был сделать!
Боб Денни

@ BlueRaja-DannyPflughoeft При слабых событиях у вас есть еще одна ошибка - вы не можете подписаться на лямбды. Вы не можете написатьtimer.Tick += (s, e,) => { Console.WriteLine(s); }
Арк-кун

@ Арк-кун, да, лямбды делают это еще сложнее, вам придется сохранить свою лямбду в переменной и использовать ее в своем коде разрыва. Разве это не разрушает простоту написания лямбд?
Тимоти Уолтерс

63

Возможно, на самом деле это не уловка, потому что поведение написано четко в MSDN, но однажды сломало мне шею, потому что я нашел это довольно нелогичным:

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

Этот парень оставляет "nice.pic"файл заблокированным, пока изображение не будет уничтожено. В то время, когда я сталкивался с этим, я думал, что было бы неплохо загружать значки на лету и не осознавал (сначала), что у меня появилось множество открытых и заблокированных файлов! Изображение отслеживает, откуда он загрузил файл ...

Как это решить? Я думал, что один лайнер сделает работу. Я ожидал дополнительный параметр для FromFile(), но не имел ни одного, поэтому я написал это ...

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

10
Я согласен, что это поведение не имеет смысла. Я не могу найти какое-либо объяснение этому, кроме "это поведение является намеренным".
MusiGenesis

1
Да, и что хорошего в этом обходном пути, если вы попытаетесь вызвать Image.ToStream (я забыл точное имя), позже он не сработает.
Джошуа

55
нужно проверить какой-то код. Brb.
Эсбен Сков Педерсен

7
@EsbenSkovPedersen Такой простой, но смешной и сухой комментарий. Сделал мой день.
Инишир

51

Если вы посчитаете ASP.NET, я бы сказал, что жизненный цикл веб-форм - это довольно большой вопрос для меня. Я потратил бесчисленные часы на отладку плохо написанного кода веб-форм только потому, что многие разработчики просто не понимают, когда использовать какой обработчик событий (включая меня, к сожалению).


26
Вот почему я перешел в MVC ... головные боли от состояния
зрения

29
Был целый другой вопрос, посвященный конкретно темам ASP.NET (вполне заслуженно). Базовая концепция ASP.NET (веб-приложения кажутся разработчикам Windows-приложениями) настолько ужасно ошибочна, что я не уверен, что это даже считается «ошибкой».
MusiGenesis

1
MusiGenesis Я бы хотел сто раз проголосовать за ваш комментарий.
csauve

3
@MusiGenesis Теперь это кажется ошибочным, но в то время люди хотели, чтобы их веб-приложения (ключевые слова - приложения - ASP.NET WebForms на самом деле не были предназначены для размещения блога) должны вести себя так же, как и их Windows-приложения. Это только изменилось относительно недавно, и многие люди все еще "не совсем там". Вся проблема заключалась в том , что абстракция была слишком дырявой - веб - не ведет себя как настольное приложение так много , что приводит к путанице в почти всех.
Луаан

1
По иронии судьбы первое, что я увидел в ASP.NET, было видео от Microsoft, демонстрирующее, как легко можно создать сайт блога с помощью ASP.NET!
MusiGenesis

51

перегруженные операторы == и нетипизированные контейнеры (массивы, наборы данных и т. д.):

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

Решения?

  • всегда использовать string.Equals(a, b) при сравнении типов строк

  • с использованием обобщений, как, List<string>чтобы убедиться, что оба операнда являются строками.


6
У вас есть лишние пробелы, которые делают все неправильно, но если вы уберете пробелы, последняя строка все равно будет истинной, так как «my» + «string» по-прежнему является константой.
Джон Скит

1
извед! ты прав :) хорошо, я немного отредактировал.
Джимми

предупреждение генерируется в таких случаях.
Чакрит

11
Да, одним из самых больших недостатков языка C # является оператор == в классе Object. Они должны были заставить нас использовать ReferenceEquals.
erikkallen

2
К счастью, с 2.0 у нас были дженерики. Меньше беспокоиться, если вы используете List <string> в приведенном выше примере вместо ArrayList. Плюс, мы получили производительность от этого, ура! Я всегда выискиваю старые ссылки на ArrayLists в нашем устаревшем коде.
JoelC

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

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

Мораль истории: инициализаторы поля не запускаются при десериализации объекта


8
Да, я ненавижу сериализацию .NET за то, что не запускаю конструктор по умолчанию. Я хотел бы, чтобы было невозможно построить объект без вызова каких-либо конструкторов, но, увы, это не так.
Роман Старков

45

DateTime.ToString ("дд / мм / гггг") ; На самом деле это не всегда даст вам дд / мм / гггг, но вместо этого он будет учитывать региональные настройки и заменять разделитель даты в зависимости от того, где вы находитесь. Таким образом, вы можете получить дд-мм-гггг или что-то подобное.

Правильный способ сделать это - использовать DateTime.ToString ("dd '/' MM '/' yyyy");


DateTime.ToString ("r") должен преобразовываться в RFC1123, который использует GMT. Время по Гринвичу составляет доли секунды от UTC, и все же спецификатор формата «r» не преобразуется в UTC , даже если рассматриваемый DateTime указан как Local.

Это приводит к следующей ошибке (зависит от того, как далеко ваше местное время от UTC):

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

Упс!


19
Изменено мм на ММ - мм - минуты, а ММ - месяцы. Еще одна ошибка, я думаю ...
Коби

1
Я мог видеть, как это было бы путаницей, если бы вы не знали об этом (я не знал) ... но я пытаюсь выяснить, когда вам нужно поведение, когда вы специально пытаетесь напечатать дату, которая не соответствует вашим региональным настройкам.
Беска

6
@Beska: поскольку вы пишете в файл, он должен быть в определенном формате с указанным форматом даты.
GvS

11
Я считаю, что локализация дефолтов хуже, чем наоборот. По крайней мере, разработчик полностью игнорировал локализацию, код работает на машинах, локализованных по-разному. Таким образом, код, вероятно, не работает.
Джошуа

32
На самом деле, я верю, что правильный способ сделать этоDateTime.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
BlueRaja - Дэнни Пфлюгофт

44

Я видел этот пост, опубликованный на днях, и я думаю, что он довольно туманный и болезненный для тех, кто не знает

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

Так как это вернет 0, а не 1, как многие ожидали бы


37
Я надеюсь, что это на самом деле не кусало бы людей - я действительно надеюсь, что они не напишут это во-первых! (В любом случае, это интересно.)
Джон Скит,

12
Я не думаю, что это очень неясно ...
Крис Марасти-Георг

10
По крайней мере, в C # результаты определяются, если они неожиданные. В C ++ это может быть 0 или 1, или любой другой результат, включая завершение программы!
Джеймс Керран

7
Это не гоча; x = x ++ -> x = x, затем увеличение x .... x = ++ x -> увеличение x затем x = x
Кевин

28
@ Кевин: Я не думаю, что это так просто. Если x = x ++ эквивалентны x = x, за которым следует x ++, то результатом будет x = 1. Вместо этого я думаю, что сначала происходит вычисление выражения справа от знака равенства (с 0), затем x увеличивается (давая x = 1), и, наконец, присваивание выполняется (давая x = 0 еще раз).
Тим Гудман

39

Я немного опоздал на эту вечеринку, но у меня есть две ошибки, которые недавно меня укусили:

DateTime разрешение

Свойство Ticks измеряет время в 10 миллионных долях секунды (100 наносекундных блоков), однако разрешение не 100 наносекунд, оно составляет около 15 мс.

Этот код:

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

даст вам вывод (например):

0
0
0
0
0
0
0
156254
156254
156254

Точно так же, если вы посмотрите на DateTime.Now.Millisecond, вы получите значения в округленных кусках по 15.625 мс: 15, 31, 46 и т. Д.

Это конкретное поведение варьируется от системы к системе , но в этом API даты и времени есть другие ошибки, связанные с разрешением .


Path.Combine

Отличный способ объединить пути к файлам, но он не всегда ведет себя так, как вы ожидаете.

Если второй параметр начинается с \символа, он не даст вам полный путь:

Этот код:

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));

Дает вам этот вывод:

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

17
Квантование времен с интервалами ~ 15 мсек не происходит из-за недостаточной точности в базовом механизме синхронизации (я не упомянул об этом ранее). Это потому, что ваше приложение работает в многозадачной ОС. Windows проверяет ваше приложение каждые 15 мс или около того, и в течение небольшого отрезка времени ваше приложение обрабатывает все сообщения, которые были поставлены в очередь с момента последнего кусочка. Все ваши вызовы внутри этого среза возвращаются в одно и то же время, поскольку все они выполняются в одно и то же время.
MusiGenesis

2
@MusiGenesis: Я знаю (сейчас), как это работает, но мне кажется вводящим в заблуждение иметь такую ​​точную меру, которая на самом деле не настолько точна. Это все равно, что сказать, что я знаю свой рост в нанометрах, когда на самом деле просто округляю его до ближайших десяти миллионов.
Дамовиза

7
DateTime вполне способен хранить до одного тика; это DateTime.Now, который не использует эту точность.
Рубен

16
Дополнительный символ '\' понятен многим людям, работающим с Unix / Mac / Linux. В Windows, если есть ведущий '\', это означает, что мы хотим перейти к корню диска (то есть C :), попробуйте это в CDкоманде, чтобы понять, что я имею в виду .... 1) Перейти к C:\Windows\System322) Тип CD \Users3) Вау! Теперь вы в C:\Users... ПОЛУЧИЛИ ЭТО? ... Path.Combine (@ "C: \ Windows \ System32", @ "\ Users") должен возвращаться, \Usersчто в точности означает[current_drive_here]:\Users
chakrit

8
Даже без «сна» это происходит так же. Это не имеет ничего общего с планированием приложения каждые 15 мс. Нативная функция, вызываемая DateTime.UtcNow, GetSystemTimeAsFileTime, имеет плохое разрешение.
Джимбо

38

Когда вы запускаете процесс (используя System.Diagnostics), который пишет в консоль, но вы никогда не читаете поток Console.Out, после определенного объема вывода ваше приложение будет зависать.


3
То же самое может произойти, когда вы перенаправляете как stdout, так и stderr и используете два вызова ReadToEnd по очереди. Для безопасной обработки stdout и stderr вы должны создать поток чтения для каждого из них.
Себастьян М

34

Нет ярлыков операторов в Linq-To-Sql

Смотрите здесь .

Короче говоря, внутри условного предложения запроса Linq-To-Sql нельзя использовать условные ярлыки, такие как ||и, &&чтобы избежать исключений с нулевой ссылкой; Linq-To-Sql оценивает обе стороны оператора OR или AND, даже если первое условие устраняет необходимость оценивать второе условие!


8
TIL. BRB, повторно оптимизирую несколько сотен запросов LINQ ...
tsilb

30

Использование параметров по умолчанию с виртуальными методами

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();

Вывод:
производная база


10
Странно, я думал, что это совершенно очевидно. Если объявленный тип есть Base, откуда компилятору получить значение по умолчанию, если нет Base? Я бы подумал, что немного сложнее, что значение по умолчанию может отличаться, если объявленный тип является производным типом, даже если вызываемый (статически) метод является базовым методом.
Тимви,

1
почему одна реализация метода получает значение по умолчанию другой реализации?
staafl

1
@staafl Аргументы по умолчанию разрешаются во время компиляции, а не во время выполнения.
fredoverflow

1
Я бы сказал, что это правило - параметры по умолчанию - люди часто не понимают, что они решаются во время компиляции, а не во время выполнения.
Луаан

4
@FredOverflow, мой вопрос был концептуальным. Хотя поведение имеет смысл в реализации, оно не является интуитивно понятным и вероятным источником ошибок. ИМХО, компилятор C # не должен позволять изменять значения параметров по умолчанию при переопределении.
Staafl

27

Значения объектов в изменяемых коллекциях

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

mypoints[i].x = 10;

не имеет никакого эффекта

mypoints[i]возвращает копию Pointобъекта значения. C # счастливо позволяет вам изменить поле копии. Молча ничего не делая.


Обновление: это, кажется, исправлено в C # 3.0:

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

6
Я могу понять, почему это сбивает с толку, учитывая, что он действительно работает с массивами (вопреки вашему ответу), но не с другими динамическими коллекциями, такими как List <Point>.
Лассе В. Карлсен

2
Вы правы. Спасибо. Я исправил свой ответ :). arr[i].attr=специальный синтаксис для массивов, который нельзя кодировать в контейнерах библиотеки; (. Почему (<выражение значения>). attr = <expr> вообще разрешен? Может ли это когда-нибудь иметь смысл?
Бьярке Эберт,

1
@Bjarke Ebert: В некоторых случаях это имеет смысл, но, к сожалению, компилятор не может их идентифицировать и разрешить. Пример сценария использования: неизменяемая структура, которая содержит ссылку на квадратный двумерный массив вместе с индикатором «поворот / переворот». Сама структура была бы неизменной, поэтому запись в элемент экземпляра, доступного только для чтения, должна быть в порядке, но компилятор не будет знать, что установщик свойств фактически не собирается писать структуру, и, следовательно, не допустит этого ,
суперкат

25

Возможно, не самое худшее, но некоторые части .net Framework используют градусы, в то время как другие используют радианы (и документация, которая появляется с Intellisense, никогда не говорит вам, что, вы должны посетить MSDN, чтобы узнать)

Всего этого можно было бы избежать, если бы Angleвместо этого был класс ...


Я удивлен, что получил так много голосов, учитывая, что мои другие ошибки значительно хуже, чем это
BlueRaja - Danny Pflughoeft

22

Для программистов на C / C ++ переход на C # является естественным. Тем не менее, самая большая проблема, с которой я столкнулся лично (и видел, как другие делали такой же переход), не полностью понимает разницу между классами и структурами в C #.

В C ++ классы и структуры идентичны; они отличаются только видимостью по умолчанию, где классы по умолчанию имеют частную видимость, а структуры по умолчанию - публичную видимость. В C ++ это определение класса

    class A
    {
    public:
        int i;
    };

функционально эквивалентно этому определению структуры.

    struct A
    {
        int i;
    };

Однако в C # классы являются ссылочными типами, а структуры - типами значений. Это делает БОЛЬШУЮ разницу в (1) решении, когда использовать один над другим, (2) тестировании равенства объектов, (3) производительности (например, бокс / распаковка) и т. Д.

В Интернете есть все виды информации, связанной с различиями между ними (например, здесь ). Я настоятельно рекомендую всем, кто осуществляет переход на C #, по крайней мере, иметь практические знания о различиях и их последствиях.


13
Итак, худший вопрос: люди не потрудились потратить время на изучение языка, прежде чем использовать его?
BlueRaja - Дэнни Пфлюгофт

3
@ BlueRaja-DannyPflughoeft Больше похоже на классическую хитрость явно похожих языков - они используют очень похожие ключевые слова и во многих случаях синтаксис, но работают совсем по-другому.
Луаан

19

Сборка мусора и утилизация (). Хотя вам не нужно ничего делать для освобождения памяти , вам все равно нужно освободить ресурсы с помощью Dispose (). Это очень легко забыть, когда вы используете WinForms или отслеживаете объекты любым способом.


2
Блок using () аккуратно решает эту проблему. Всякий раз, когда вы видите вызов Dispose, вы можете немедленно и безопасно рефакторинг использовать, используя ().
Джереми Фрей

5
Я думаю, что проблема заключалась в правильной реализации IDisposable.
Марк Брэкетт

4
С другой стороны, привычка using () может неожиданно вас укусить, как при работе с PInvoke. Вы не хотите распоряжаться чем-то, на что API все еще ссылается.
MusiGenesis

3
Правильно реализовать IDisposable очень сложно, и понять, что даже самый лучший совет, который я нашел по этому вопросу (Руководство по .NET Framework), может быть сложно применить, пока вы, наконец, не «получите его».
Ужасно

1
Лучший совет, который я когда-либо нашел в отношении IDisposable, исходит от Стивена Клири, включая три простых правила и подробную статью о IDisposable
Роман Старков,

19

Массивы реализуют IList

Но не реализуйте это. Когда вы звоните Add, он говорит вам, что это не работает. Так почему класс реализует интерфейс, если он не может его поддерживать?

Компилируется, но не работает:

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

У нас много этой проблемы, потому что сериализатор (WCF) превращает все ILists в массивы, и мы получаем ошибки времени выполнения.


8
ИМХО, проблема в том, что у Microsoft недостаточно интерфейсов, определенных для коллекций. ИМХО, он должен иметь iEnumerable, iMultipassEnumerable (поддерживает сброс и гарантирует совпадение нескольких проходов), iLiveEnumerable (будет иметь частично определенную семантику, если коллекция изменяется во время перечисления - изменения могут появляться или не появляться в перечислении, но не должны вызывать фиктивные результаты или исключения), iReadIndexable, iReadWriteIndexable и т. д. Поскольку интерфейсы могут «наследовать» другие интерфейсы, это не добавило бы много дополнительной работы, если таковые имеются (это сохранит заглушки NotImplemented).
Суперкат

@supercat, это было бы чертовски сложно для начинающих и некоторых давних программистов. Я думаю, что коллекции .NET и их интерфейсы удивительно элегантны. Но я ценю ваше смирение. ;)
Иордания

@Jordan: Со времени написания вышеизложенного я решил, что лучшим подходом было бы иметь и то IEnumerable<T>и другое и IEnumerator<T>поддерживать Featuresсвойство, а также некоторые «необязательные» методы, полезность которых будет определяться тем, что сообщают «Особенности». Тем не менее, я придерживаюсь своей основной позиции, заключающейся в том, что в некоторых случаях для получения кода IEnumerable<T>требуются более сильные обещания, чем для IEnumerable<T>предоставления. Вызов ToListможет привести к выполнению IEnumerable<T>таких обещаний, но во многих случаях будет излишне дорогим. Я бы сказал, что там должно быть ...
суперкат

... средство, с помощью которого получающий код IEnumerable<T>может сделать копию содержимого, если это необходимо, но может воздерживаться от этого без необходимости.
суперкат

Ваш вариант абсолютно не читается. Когда я вижу IList в коде, я знаю, с чем я работаю, вместо того, чтобы проверять свойство Features. Программисты любят забывать, что важной особенностью кода является то, что он может быть прочитан людьми, а не только компьютерами. Пространство имен коллекций .NET не является идеальным, но это хорошо, и иногда нахождение наилучшего решения не является вопросом идеального подбора принципа. Одним из худших кодов, с которыми я когда-либо работал, был код, который пытался идеально соответствовать DRY. Я сдал его и переписал. Это был просто плохой код. Я бы не хотел использовать ваш фреймворк вообще.
Джордан

18

область видимости переменных цикла 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());

печатает пять "амет", в то время как следующий пример работает нормально

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
Это по сути эквивалентно примеру Джона с анонимными методами.
Мердад Афшари

3
За исключением того, что это еще более запутанно с foreach, где переменную "s" легче смешивать с переменной области видимости. С обычными циклами for переменная индекса явно одинакова для каждой итерации.
Микко Рантанен

2
blogs.msdn.com/ericlippert/archive/2009/11/12/… и да, желаю, чтобы переменная была «правильно» определена.
Роман Старков


По сути, вы просто печатаете одну и ту же переменную снова и снова, не меняя ее.
Джордан

18

MS SQL Server не может обрабатывать даты до 1753 года. Важно отметить, что это не синхронизировано с DateTime.MinDateконстантой .NET , которая составляет 1/1/1. Поэтому, если вы попытаетесь сохранить разум, искаженную дату (как недавно произошло со мной при импорте данных) или просто дату рождения Вильяма Завоевателя, у вас будут проблемы. Для этого нет встроенного обходного пути; если вам, вероятно, понадобится работать с датами до 1753 года, вам нужно написать собственный обходной путь.


17
Откровенно говоря, я думаю, что MS SQL Server имеет это право, а .Net не так. Если вы проведете исследование, то узнаете, что даты до 1751 года становятся забавными из-за изменений в календаре, пропущенных дней и т. Д. У большинства СУБД есть определенная точка отсечения. Это должно дать вам отправную точку: ancestry.com/learn/library/article.aspx?article=3358
NotMe

11
Кроме того, дата 1753 года. Это был первый случай, когда у нас был непрерывный календарь без пропущенных дат. В SQL 2008 появился тип даты Date и datetime2, который может принимать даты с 01.01.01 по 31.12.9999. Однако сравнения дат с использованием этих типов следует рассматривать с подозрением, если вы действительно сравниваете даты до 1753 года.
NotMe

Ах да, 1753, исправлено, спасибо.
Шауль Бер

Действительно ли имеет смысл сравнивать даты с такими датами? Я имею в виду, что для Канала Истории это имеет большой смысл, но я не вижу себя желающим узнать точный день недели, когда была открыта Америка.
Камило Мартин

5
В Википедии, посвященной юлианскому дню, вы можете найти основную программу CALJD.BAS, состоящую из 13 строк, которая может рассчитывать даты примерно до 5000 г. до н.э., принимая во внимание високосные дни и пропущенные дни в 1753 году. Поэтому я не понимаю, почему «современные "системы, подобные SQL2008, должны работать хуже. Возможно, вас не заинтересует правильное представление даты в 15 веке, но другие могут заинтересоваться, и наше программное обеспечение должно справиться с этим без ошибок. Еще одна проблема - високосные секунды. , ,
Роланд

18

Nasty Linq Caching Gotcha

Смотрите мой вопрос, который привел к этому открытию, и блогера, который обнаружил проблему.

Короче говоря, DataContext хранит кэш всех объектов Linq-to-Sql, которые вы когда-либо загружали. Если кто-либо внесет какие-либо изменения в запись, которую вы ранее загрузили, вы не сможете получить последние данные, даже если вы явно перезагрузите запись!

Это происходит из-за свойства, вызываемого ObjectTrackingEnabledдля DataContext, которое по умолчанию имеет значение true. Если вы установите для этого свойства значение false, запись будет загружаться заново каждый раз ... НО ... вы не можете сохранить какие-либо изменения в этой записи с помощью SubmitChanges ().

ПОПАЛСЯ!


Iv провел полтора дня (и кучу волос!), Гоняясь за этим БУГОМ ...
Хирургический кодер

Это называется конфликтом параллелизма, и сегодня он все еще остаётся ошибкой, хотя сейчас есть определенные пути решения этой проблемы, хотя они, как правило, немного тяжелы. DataContext был кошмаром. О_о
Джордан

17

Контракт на Stream.Read - это то, что, как я видел, сбивает с толку многих людей:

// 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);

Причина, по которой это неверно, заключается в том, что Stream.Readбудет считываться не более указанного количества байтов, но полностью свободна для чтения всего 1 байта, даже если до конца потока доступны еще 7 байтов.

Это не помогает , что это выглядит так , подобные Stream.Write, которые в гарантируемом написали все байты , если он возвращается без исключения. Также не помогает, что приведенный выше код работает почти все время . И, конечно, не помогает, что не существует готового, удобного метода для правильного чтения ровно N байтов.

Итак, чтобы закрыть дыру и повысить осведомленность об этом, вот пример правильного способа сделать это:

    /// <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
Или, в вашем явном примере: var r = new BinaryReader(stream); ulong data = r.ReadUInt64();. У BinaryReader тоже есть FillBufferметод ...
jimbobmcgee

15

Мероприятия

Я никогда не понимал, почему события являются языковой особенностью. Они сложны в использовании: вам нужно проверить нулевое значение перед звонком, вам нужно отменить регистрацию (самостоятельно), вы не можете узнать, кто зарегистрирован (например: я зарегистрировался?). Почему событие не просто класс в библиотеке? В основном специализированный List<delegate>?


1
Кроме того, многопоточность является болезненным. Все эти проблемы, кроме нуля, исправлены в CAB (чьи возможности действительно должны быть встроены в язык) - события объявляются глобально, и любой метод может объявить себя «подписчиком» любого события. Моя единственная проблема с CAB - это то, что глобальные имена событий являются строками, а не перечислениями (которые могут быть исправлены более интеллектуальными перечислениями, как в Java, которые по своей природе работают как строки!) . САВ трудно установить, но есть простой клон с открытым исходным кодом доступен здесь .
BlueRaja - Дэнни Пфлюгофт

3
Мне не нравится реализация событий .net. Подписка на событие должна обрабатываться путем вызова метода, который добавляет подписку и возвращает IDisposable, который, когда Dispose'd, удалит подписку. Нет необходимости в специальной конструкции, объединяющей метод «добавить» и «удалить», семантика которой может быть несколько хитрой, особенно если кто-то пытается добавить, а затем удалить многоадресный делегат (например, добавить «B», затем «AB», затем удалить "B" (оставляя "BA") и "AB" (все еще оставляя "BA"). Упс.
суперкат

@supercat Как бы вы переписали button.Click += (s, e) => { Console.WriteLine(s); }?
Арк-кун

Если бы я мог иметь возможность отписаться отдельно от других событий, IEventSubscription clickSubscription = button.SubscribeClick((s,e)=>{Console.WriteLine(s);});и отписаться через clickSubscription.Dispose();. Если бы мой объект сохранил все подписки на протяжении всей своей жизни, MySubscriptions.Add(button.SubscribeClick((s,e)=>{Console.WriteLine(s);}));а затем MySubscriptions.Dispose()убил бы все подписки.
суперкат

@ Ark-kun: необходимость хранить объекты, которые инкапсулируют внешние подписки, может показаться неприятной, но если рассматривать подписки как объекты, можно объединить их с типом, который может обеспечить их очистку, что в противном случае очень сложно.
суперкат

14

Сегодня я исправил ошибку, которая долго отсутствовала. Ошибка была в универсальном классе, который использовался в многопоточном сценарии, а статическое поле int использовалось для обеспечения синхронизации без блокировки с использованием Interlocked. Ошибка была вызвана тем, что каждый экземпляр универсального класса для типа имеет собственную статическую переменную. Таким образом, каждый поток получил свое собственное статическое поле и не использовал блокировку, как предполагалось.

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);
    }
}

Это печатает 5 10 5


5
у вас может быть неуниверсальный базовый класс, который определяет статику и наследует от него дженерики. Хотя я никогда не влюблялся в такое поведение в C # - я все еще помню долгие часы отладки некоторых шаблонов C ++ ... Eww! :)
Паулюс

7
Странно, я думал, что это очевидно. Просто подумайте, что он должен делать, если бы iимел тип T.
Тимви,

1
Параметр типа является частью Type. SomeGeneric<int>отличается от типа SomeGeneric<string>; так что конечно у каждого своеpublic static int i
радарбоб

13

Перечисляемые могут быть оценены более одного раза

Это укусит вас, когда у вас будет перечислимое с ленивым перечислением, и вы дважды итерируете его и получите разные результаты. (или вы получаете те же результаты, но они выполняются дважды без необходимости)

Например, при написании определенного теста мне понадобилось несколько временных файлов для проверки логики:

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);

Представь мое удивление когда File.Delete(file)кидает FileNotFound!!

Здесь происходит то, что filesперечислимое повторяется дважды (результаты первой итерации просто не запоминаются), и на каждой новой итерации вы будете повторно вызыватьPath.GetTempFilename() поэтому вы получите другой набор временных имен файлов.

Решение, конечно, состоит в том, чтобы стремиться перечислить значение, используя ToArray()или ToList():

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

Это даже страшнее, когда вы делаете что-то многопоточное, например:

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

и вы узнаете, content.Lengthвсе еще 0 после всех записей !! Затем вы начинаете строго проверять, что у вас нет состояния гонки, когда ... после одного потерянного часа ... вы поняли, что это просто крошечная Перечисляемая вещь, которую вы забыли ...


Это по замыслу. Это называется отложенным исполнением. Помимо прочего, он предназначен для имитации конструкций TSQL. Каждый раз, когда вы выбираете из представления SQL, вы получаете разные результаты. Это также позволяет создавать цепочки, что полезно для удаленных хранилищ данных, таких как SQL Server. В противном случае x.Select.Where.OrderBy отправит 3 отдельные команды в базу данных ...
as9876

@AYS ты пропустил слово "Gotcha" в заголовке вопроса?
Чакрит

Я думал, что «Гоча» означает недосмотр дизайнеров, а не что-то преднамеренное.
as9876

Может быть, должен быть другой тип для не перезапускаемых IEnumerables. Как, AutoBufferedEnumerable? Это можно легко реализовать. Похоже, это связано с недостатком знаний программиста, я не думаю, что с текущим поведением что-то не так.
Eldritch Conundrum

13

Просто нашел странный, который на некоторое время застрял в отладке:

Вы можете увеличивать ноль для обнуляемого целого, не выбрасывая исключение, и значение остается нулевым.

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

Это результат того, как C # использует операции для обнуляемых типов. Это немного похоже на NaN, поглощающее все, что вы кидаете в него.
IllidanS4 хочет вернуть Монику

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!"

Да, это поведение задокументировано, но это определенно не делает его правильным.


5
Я не согласен - когда слово стоит во всех заглавных буквах, оно может иметь особое значение, что вы не хотите путать с Заголовком дела, например, «президент США» -> «президент США», а не «президент Соединенные Штаты Америки".
Шаул Бер

5
@Shaul: В этом случае они должны указать это как параметр, чтобы избежать путаницы, потому что я никогда не встречал никого, кто ожидал бы такого поведения раньше времени - что делает это ошибкой !
BlueRaja - Дэнни Пфлюгофт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.