Ответ 1
private int myVar;
public int MyVar
{
get { return MyVar; }
}
Blammo. Ваше приложение отключается без трассировки стека. Случается постоянно.
(обратите внимание на капитал MyVar
вместо строчного MyVar
в получателе.)
Недавно я работал с объектом DateTime
и писал что-то вроде этого:
DateTime dt = DateTime.Now;
dt.AddDays(1);
return dt; // still today date! WTF?
Документация intellisense для AddDays()
говорит, что добавляет день к дате, а это не означает, что на самом деле она возвращает дату с добавленным к ней днем, поэтому вам нужно написать ее так:
DateTime dt = DateTime.Now;
dt = dt.AddDays(1);
return dt; // tomorrow date
Этот укусил меня несколько раз раньше, поэтому я подумал, что было бы полезно каталогизировать худшие С# -часы.
private int myVar;
public int MyVar
{
get { return MyVar; }
}
Blammo. Ваше приложение отключается без трассировки стека. Случается постоянно.
(обратите внимание на капитал MyVar
вместо строчного MyVar
в получателе.)
Type.GetType
Тот, который я видел, кусает много людей Type.GetType(string)
. Они задаются вопросом, почему он работает для типов в собственной сборке, а некоторые типы вроде System.String
, но не System.Windows.Forms.Form
. Ответ заключается в том, что он просматривается только в текущей сборке и в mscorlib
.
Анонимные методы
С# 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();
}
}
}
Отложенное выполнение блоков итератора
Этот "бедный человек unit test" не проходит - почему бы и нет?
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
не запускается до тех пор, пока не будет вызван метод iterator MoveNext()
.
У меня есть некоторые другие странности на моей странице .
В результате получается множество новых разработчиков, это семантика исключений re-throw.
Сколько времени я вижу, например, код
catch(Exception e)
{
// Do stuff
throw e;
}
Проблема заключается в том, что он стирает трассировку стека и значительно затрудняет диагностику, потому что вы не можете отслеживать, откуда возникло исключение.
Правильный код - это либо оператор throw без аргументов:
catch(Exception)
{
throw;
}
Или обертывание исключения в другом и использование внутреннего исключения для получения исходной трассировки стека:
catch(Exception e)
{
// Do stuff
throw new MySpecialException(e);
}
Окно просмотра Гейзенберга
Это может сильно укусить вас, если вы делаете нагрузку по требованию, например:
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
, очевидно, null, не так ли? Ну... так... почему он пропустил условие и пошел прямо к return _myObj
?! Вы наводите указатель мыши на _myObj... и действительно, это имеет значение! Как это произошло?!
Ответ заключается в том, что ваша IDE заставила его получить значение, потому что у вас открыто окно "смотреть", особенно окно "Авто", в котором отображаются значения всех переменных/свойств, относящихся к текущей или предыдущей строке исполнения. Когда вы нажмете свою точку останова на Строке 3, окно просмотра решит, что вам будет интересно узнать значение MyObj
- так что за кулисами игнорируя любую из ваших контрольных точек, он пошел и вычислил значение MyObj
для вас - , включая вызов CreateMyObj()
, который устанавливает значение _myObj!
Вот почему я называю это Окно просмотра Гейзенберга - вы не можете наблюдать значение, не затрагивая его...:)
GOTCHA!
Изменить. Я чувствую, что комментарий @ChristianHayter заслуживает включения в главный ответ, потому что он выглядит как эффективный обходной путь для этой проблемы. Поэтому в любое время вы обладаете ленивым свойством...
Украсьте свое свойство с помощью [DebuggerBrowsable (DebuggerBrowsableState.Never)] или [DebuggerDisplay ("")]. - Христиан Хейтер
Вот еще один раз, когда я получаю:
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, если вы не делаете a = = на таймере ваша форма утечки памяти!
В этом примере ваш код разрыва должен иметь
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, я бы сказал, что жизненный цикл webforms для меня довольно большой. Я потратил бесчисленное количество часов на отладку плохо написанного кода веб-форм только потому, что многие разработчики просто не понимают, когда использовать обработчик событий (включая меня, к сожалению).
overloaded == операторы и нетипизированные контейнеры (arraylists, наборы данных и т.д.):
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>
, чтобы гарантировать, что оба операнда являются строками.
DateTime.ToString( "дд/мм/гггг" ); На самом деле не всегда будет давать вам dd/MM/yyyy, но вместо этого он учитывает региональные настройки и заменяет ваш разделитель дат в зависимости от того, где вы находитесь. Таким образом, вы можете получить dd-MM-yyyy или что-то подобное.
Правильный способ сделать это - использовать DateTime.ToString( "dd '/' MM '/' yyyy" );
DateTime.ToString( "r" ) предполагается преобразовать в RFC1123, который использует GMT. GMT находится в пределах долей секунды от UTC, но спецификатор формата "r" не конвертируется в UTC, даже если рассматриваемый DateTime указан как локальный.
В результате получается следующая информация (зависит от того, насколько далеко ваше местное время от UTC):
DateTime.Parse("Tue, 06 Sep 2011 16:35:12 GMT").ToString("r")
> "Tue, 06 Sep 2011 17:35:12 GMT"
Упс!
[Serializable]
class Hello
{
readonly object accountsLock = new object();
}
//Do stuff to deserialize Hello with BinaryFormatter
//and now... accountsLock == null ;)
Мораль истории: инициализаторы полей не запускаются при десериализации объекта
Я видел, что это было опубликовано на днях, и я думаю, что это довольно неясное и болезненное для тех, кто не знает
int x = 0;
x = x++;
return x;
Так как это вернет 0 и не 1, как ожидалось бы большинство
Я немного опаздываю на эту вечеринку, но у меня есть две gotchas, которые недавно укусили меня:
Свойство 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.625ms: 15, 31, 46 и т.д.
Это конкретное поведение варьируется от системы к системе, но есть другие исправления, связанные с разрешением. API даты/времени.
Отличный способ объединить пути файлов, но он не всегда ведет себя так, как вы ожидали.
Если второй параметр начинается с символа \
, он не даст вам полный путь:
Этот код:
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
. С# счастливо позволяет вам изменить поле копии. Безмолвно ничего не делает.
Update: Это, по-видимому, исправлено в С# 3.0:
Cannot modify the return value of 'System.Collections.Generic.List<Foo>.this[int]' because it is not a variable
Возможно, не самое худшее, но некоторые части .net framework используют градусы, в то время как другие используют radians (и документация, которая появляется в Intellisense, никогда не сообщает вам, что вам нужно посетить MSDN, чтобы узнать)
Все это можно было бы избежать, если вместо этого был класс Angle
...
Для программистов на C/С++ переход на С# является естественным. Однако самая большая проблема, с которой я столкнулся лично (и видел с другими, делающими тот же переход), не полностью понимает разницу между классами и структурами на С#.
В С++ классы и структуры идентичны; они отличаются только видимостью по умолчанию, при которой классы по умолчанию закрывают видимость и структуры по умолчанию для публичной видимости. В С++ это определение класса
class A
{
public:
int i;
};
функционально эквивалентен этому описанию структуры.
struct A
{
int i;
};
В С#, однако, классы являются ссылочными типами, а structs - типами значений. Это делает разницу БОЛЬШОЙ в (1) решающей, когда использовать один над другим, (2) тестировать равенство объекта, (3) производительность (например, бокс/распаковка) и т.д.
В Интернете есть все виды информации, связанные с различиями между ними (например, здесь). Я бы настоятельно рекомендовал любому, кто перешел на С#, по крайней мере, иметь рабочее знание о различиях и их последствиях.
Сбор мусора и утилизация(). Хотя вам не нужно ничего делать, чтобы освободить память, вам все равно придется освобождать ресурсы через Dispose(). Это очень легко забыть, когда вы используете WinForms или отслеживаете объекты каким-либо образом.
диапазон переменных foreach loops!
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());
печатает пять "amet", в то время как следующий пример отлично работает
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());
MS SQL Server не может обрабатывать даты до 1753. Значительно, что это не синхронизируется с константой .NET DateTime.MinDate
, которая равна 1/1/1. Поэтому, если вы попытаетесь сохранить отзыв, неверную дату (как это недавно случилось со мной при импорте данных) или просто дату рождения Уильяма Завоевателя, у вас будут проблемы. Для этого нет встроенного обходного пути; если вам нужно будет работать с датами до 1753 года, вам нужно написать свое обходное решение.
The Nasty Linq Caching Gotcha
См. мой вопрос, который привел к этому открытию, и блоггер, который обнаружил проблему.
Короче говоря, DataContext хранит кеш всех объектов Linq-to-Sql, которые вы когда-либо загружали. Если кто-либо еще внесет какие-либо изменения в ранее загруженную запись, вы не сможете получить последние данные, , даже если вы явно перезагрузите запись!
Это из-за свойства, называемого ObjectTrackingEnabled
в DataContext, которое по умолчанию является истинным. Если вы установите для этого свойства значение false, запись будет загружаться заново каждый раз... НО... вы не можете сохранять какие-либо изменения в этой записи с помощью SubmitChanges().
GOTCHA!
Массивы реализуют IList
Но не реализуйте его. Когда вы вызываете "Добавить", он сообщает вам, что он не работает. Итак, почему класс реализует интерфейс, если он не может его поддерживать?
Скомпилирует, но не работает:
IList<int> myList = new int[] { 1, 2, 4 };
myList.Add(5);
У нас эта проблема много, потому что сериализатор (WCF) превращает все ILists в массивы, и мы получаем ошибки времени выполнения.
Контракт на 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;
}
События
Я никогда не понимал, почему события являются языковой особенностью. Они сложны в использовании: вам нужно проверить нуль перед вызовом, вам нужно отменить регистрацию (самостоятельно), вы не можете узнать, кто зарегистрирован (например: зарегистрирован ли я?). Почему это событие не является классом в библиотеке? В основном специализированный List<delegate>
?
Сегодня я исправил ошибку, которая ускользала в течение длительного времени. Ошибка была в родовом классе, который использовался в многопоточном сценарии, и статическое поле int использовалось для обеспечения синхронизации без блокировки с использованием блокировки. Ошибка была вызвана тем, что каждый экземпляр родового класса для типа имеет свою собственную статичность. Таким образом, каждый поток получил свое собственное статическое поле и не использовал блокировку, как предполагалось.
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
Перечисления можно оценивать более одного раза
Это вас укусит, когда вы будете перечислить лениво перечисляемые, и вы повторите его дважды и получите разные результаты. (или вы получаете те же результаты, но это выполняется дважды без необходимости)
Например, при написании определенного теста мне понадобилось несколько файлов temp для проверки логики:
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()
, чтобы вы получили другую набор temp файлов.
Конечно, решение состоит в том, чтобы запрограммировать значение с помощью 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 после всех записей!! Затем вы начинаете тщательно проверять, что у вас нет состояния гонки, когда... после одного потраченного впустую часа... вы выяснили, что это крошечная маленькая перечислимая информация, которую вы забыли...
Просто нашел странный, который заставлял меня откладывать на некоторое время:
Вы можете увеличивать значение null для NULL int без выделения исключения, а значение остается равным нулю.
int? i = null;
i++; // I would have expected an exception but runs fine and stays as null
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!"
Да, это поведение задокументировано, но это, конечно, не делает это правильно.