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

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

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

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

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

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

61 ответ

Решение
private int myVar;
public int MyVar
{
    get { return MyVar; }
}

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

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

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() Метод сначала вызывается.

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

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

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

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 ("<загружено по требованию>")]. - Кристиан Хейтер

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

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

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

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

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

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

catch(Exception)
{
    throw;
}

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

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

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

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

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

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

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

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

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

timer.Tick -= TimerTickEventHandler;

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

Возможно, на самом деле это не так, потому что поведение написано четко в 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);
}

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

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

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> чтобы оба операнда были строками.

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

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

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

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"

Упс!

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

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

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

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

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\

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

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

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

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

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

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

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

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

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

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

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

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

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

    class A
    {
    public:
        int i;
    };

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

    struct A
    {
        int i;
    };

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

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

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

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

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

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

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

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

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

Контракт на 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;
    }

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

Nasty Linq Caching Gotcha

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

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

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

ПОПАЛСЯ!

Сегодня я исправил ошибку, которая долго отсутствовала. Ошибка была в универсальном классе, который использовался в многопоточном сценарии, а статическое поле 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

События

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

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

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

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

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

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

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

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 после всех записей!! Затем вы начинаете строго проверять, что у вас нет состояния гонки, когда... после одного потерянного часа... вы поняли, что это просто та крошечная Перечисляемая вещь, которую вы забыли...

Это супер, что я потратил 2 дня на устранение неисправностей. Он не выдавал никаких исключений, он просто разбил веб-сервер с некоторыми странными сообщениями об ошибках. Я не смог воспроизвести проблему в DEV. Более того, эксперименты с настройками сборки проекта как-то заставили его исчезнуть в PROD, а затем он вернулся. Наконец то я понял.

Сообщите мне, если вы видите проблему в следующем фрагменте кода:

private void DumpError(Exception exception, Stack<String> context)
{
    if (context.Any())
    {
        Trace.WriteLine(context.Pop());
        Trace.Indent();
        this.DumpError(exception, context);
        Trace.Unindent();
    }
    else
    {
        Trace.WriteLine(exception.Message);
    }
}

Так что, если вы цените свое здравомыслие:

!!! Никогда не ставил никакой логики в методы трассировки!!!

Код должен был выглядеть так:

private void DumpError(Exception exception, Stack<String> context)
{
    if (context.Any())
    {
        var popped = context.Pop();
        Trace.WriteLine(popped);
        Trace.Indent();
        this.DumpError(exception, context);
        Trace.Unindent();
    }
    else
    {
        Trace.WriteLine(exception.Message);
    }
}
Другие вопросы по тегам