Яка найгірша проблема в 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 # gotchas.


157
повернути DateTime.Now.AddDays (1);
crashmstr

23
AFAIK, вбудовані типи значень є незмінними, принаймні тим, що будь-який метод, включений до типу, повертає новий елемент, а не модифікувати існуючий елемент. Принаймні, я не можу придумати одного з верхівки голови, який цього не робить: все добре і послідовно.
Joel Coehoorn

6
Тип змінного значення: System.Collections.Generics.List.Enumerator :( (І так, ви можете бачити, як він веде себе дивно, якщо ви досить стараєтесь.)
Jon Skeet

13
Intellisense надає всю необхідну інформацію. Він говорить, що повертає об'єкт DateTime. Якби він просто змінив той, який ти передав, це був би недійсний метод.
Джон Крафт

20
Не обов’язково: StringBuilder.Append (...) повертає, наприклад, "це". Це досить часто в умовах вільних інтерфейсів.
Джон Скіт

Відповіді:


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

Бламмо. Ваш додаток виходить з ладу без сліду стека. Відбувається весь час.

(Зверніть увагу на капітал MyVarзамість малих літер myVar.)


112
і ТАК підходить для цього сайту :)
gbjbaanb

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

61
Я використовую автоматичні властивості там, де можу, зупиняє подібні проблеми;)
TWith2Sugars

28
Це ВЕЛИКИЙ привід використовувати префікси для своїх приватних полів (є й інші, але це добре): _myVar, m_myVar
jrista

205
@jrista: О, будь ласка, ні ... не m_ ... aargh жах ...
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; 
}

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

Правильний код - це або операція кидання без аргументів:

catch(Exception)
{
    throw;
}

Або загортання винятку в інший та використання внутрішнього виключення для отримання початкового сліду стека:

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

На щастя, мене навчили про це на своєму першому тижні хтось і знайшов це у коді старших розробників. Є: catch () {кинути; } Те саме, що і другий фрагмент коду? ловити (виняток е) {кидати; } тільки він не створює об'єкт "Виняток" і заповнює його?
StuperUser

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

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

7
Я це бачу весь час, коли люди виловлюють та повторюють винятки лише тому, що їх вчать, що вони повинні виловлювати всі винятки, не усвідомлюючи, що це буде спіймано далі в стек викликів. Це ганяє мене.
Джеймс Вестгейт

5
@Kyralessa - найбільший випадок, коли вам доведеться робити лісозаготівлю. Запишіть помилку в лові і повторно ..
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 призвів до отримання значення, оскільки у вас відкрите вікно "годинник" - особливо вікно перегляду "Авто", яке відображає значення всіх змінних / властивостей, що відповідають поточному чи попередньому рядку виконання. Коли ви потрапили на точку розриву на лінії 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 або щось подібне, щоб зрозуміти, що це таке.
Dan Diplo

3
На Перечитуючи це, я повинен задатися питанням , чому TimeSpanнавіть має в Secondsвласності взагалі. Хто взагалі дає щурячу дупу, яка секунда відрізка часу? Це довільне, залежне від одиниці значення; Я не можу уявити жодної практичної користі для цього.
MusiGenesis

2
Має сенс для мене, що TimeSpan.TotalSeconds поверне ... загальну кількість секунд за проміжок часу.
Ред С.

16
@MusiGenesis властивість корисна. Що робити, якщо я хочу відобразити розрізнений на шматки часовий проміжок часу? Скажімо, ваш часовий проміжок означає тривалість "3 години 15 хвилин 10 секунд". Як ви можете отримати доступ до цієї інформації без властивостей Seconds, Hours, Minutes?
РішенняYogi

1
У подібних API я використовував SecondsPartта SecondsTotalрозрізняв ці два.
BlueRaja - Danny Pflughoeft

80

Залишається пам'ять, тому що ви не відключили події.

Це навіть зловило деяких старших розробників, яких я знаю.

Уявіть форму WPF з великою кількістю речей, і десь там ви підписуєтесь на подію. Якщо ви не скасуєте підписку, тоді вся форма зберігається в пам’яті після закриття та де-посилання.

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

У цьому прикладі повинен бути ваш код для відмови

timer.Tick -= TimerTickEventHandler;

Цей варіант особливо складний, оскільки ви створили екземпляр DispatchTimer всередині форми WPF, тож ви можете подумати, що це буде внутрішня довідка, яку обробляє процес збору сміття ... на жаль, DispatchTimer використовує статичний внутрішній список підписок і послуг запитів у потоці користувальницького інтерфейсу, тому посилання є "власником" статичного класу.


1
Трюк полягає в тому, щоб завжди випускати всі створені підписки на події. Якщо ви почнете покладатися на те, що Форми роблять це за вас, ви можете бути впевнені, що увійдете в звичку, і одного дня забудете випустити подію десь там, де це потрібно зробити.
Джейсон Вільямс

3
Існує МС-Коннект пропозицію для слабких референтних подій тут , які б вирішити цю проблему, хоча на мій погляд , ми просто повинні повністю замінити неймовірно погану модель подій з слабосвязанних один, як це використовується САВ.
BlueRaja - Danny Pflughoeft

+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
потрібно перевірити якийсь код. Брб.
Есбен Сков Педерсен

7
@EsbenSkovPedersen Такий простий, але смішний і сухий коментар. Зробив мій день.
Inisheer

51

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


26
Ось чому я переїхав до MVC ... головного болю в зоні перегляду ...
chakrit

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
У вас є додаткові пробіли, які роблять все неправильним, але якщо ви вилучите пробіли, останній рядок все одно буде істинним, оскільки "мій" + "рядок" все ще є постійною.
Джон Скіт

1
ак! ви праві :) добре, я трохи відредагував.
Джиммі

на таких використання генерується попередження.
чакрит

11
Так, одним з найбільших недоліків мови C # є оператор == у класі Object. Вони повинні були змусити нас використовувати ReferenceEquals.
erikkallen

2
На щастя, з 2.0 року ми мали дженерики. Менше турбуватися, якщо ви використовуєте Список <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 ("dd / MM / yyyy") ; Це насправді не буде завжди дає вам dd / MM / yyyy, але замість цього воно врахує регіональні налаштування та замінить роздільник ваших дат залежно від того, де ви знаходитесь. Таким чином, ви можете отримати dd-MM-yyyy чи щось подібне.

Правильний спосіб зробити це - використовувати DateTime.ToString ("dd" / "MM" / "yyyy");


DateTime.ToString ("r") повинен перетворитися на RFC1123, який використовує GMT. 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 - Danny Pflughoeft

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
@Kevin: Я не думаю, що це все так просто. Якщо x = x ++ були еквівалентними x = x, а за ними x ++, то результат був би x = 1. Натомість, я думаю, що відбувається, спочатку оцінюється вираз праворуч від знака рівності (даючи 0), то x дорівнює збільшується (дає x = 1), і нарешті завдання виконується (даючи ще раз x = 0).
Тім Гудман

39

Я трохи запізнююсь на цю вечірку, але у мене є два готча, які обоє мене покусали нещодавно:

Роздільна здатність DateTime

Властивість Тікса вимірює час у 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\

17
Квантування часу в інтервалах ~ 15 мс не пов'язане з недостатньою точністю базового механізму синхронізації (я нехтував детальніше про це детальніше). Це тому, що ваш додаток працює у багатозадачній ОС. Windows перевіряється у вашому додатку кожні 15 хвилин або близько того, і за короткий проміжок часу ваш додаток обробляє всі повідомлення, які були в черзі з моменту останнього фрагмента. Усі ваші дзвінки в межах цього фрагменту повертаються точно в той же час, оскільки всі вони здійснюються фактично в той самий час.
MusiGenesis

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

7
DateTime цілком здатний зберігати до однієї галочки; це DateTime.Зараз, що не використовує цю точність.
Рубен

16
Додатковий '\' є прихильністю для багатьох людей unix / mac / linux. У Windows, якщо є провідний '\', це означає, що ми хочемо запустити корінь диска (тобто С :) спробувати його в CDкоманді, щоб побачити, що я маю на увазі .... 1) Перехід C:\Windows\System322) Тип CD \Users3) Woah! Тепер ти в 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? Я б подумав, що це трохи більше gotcha, що значення за замовчуванням може бути різним, якщо оголошений тип є похідним типом, хоча метод, який називається (статично), є базовим методом.
Тімві

1
чому одна реалізація методу отримає значення за замовчуванням іншої реалізації?
staafl

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

1
Я б сказав, що цей готч - це параметри за замовчуванням загалом - люди часто не розуміють, що вони вирішені під час компіляції, а не під час виконання.
Луань

4
@FredOverflow, моє запитання було концептуальним. Хоча поведінка має сенс для впровадження, вона неінтуїтивна та ймовірне джерело помилок. IMHO компілятор 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>? Чи може це коли-небудь має сенс?
Bjarke Ebert.

1
@Bjarke Ebert: Є деякі випадки, коли це мало б сенс, але, на жаль, компілятор не може їх визначити та дозволити. Приклад сценарію використання: незмінна структура, яка містить посилання на квадратний двовимірний масив разом з індикатором "повернути / перевернути". Сама структура була б непорушною, тому записування до елемента екземпляра лише для читання повинно бути добре, але компілятор не знатиме, що налаштування властивості насправді не збирається писати структуру, і, отже, не дозволить .
supercat

25

Можливо, не найгірше, але в деяких частинах .net рамки використовуються градуси, в той час як інші використовують радіани (а документація, яка з’являється разом з 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 - Danny Pflughoeft

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

19

Збір сміття та утилізація (). Хоча вам не потрібно нічого робити, щоб звільнити пам'ять , вам все одно доведеться звільнити ресурси за допомогою Dispose (). Це надзвичайно просто забути, коли ви використовуєте WinForms або відстежуєте об’єкти будь-яким способом.


2
Використовуючи () блок акуратно вирішує цю проблему. Щоразу, коли ви побачите виклик розпоряджатися, ви можете негайно та безпечно перетворювати рефактор на використання за допомогою ().
Джеремі Фрей

5
Я думаю, що концерна правильно реалізує IDisposable.
Марк Брекетт

4
З іншого боку, звичка використання () може вас несподівано вкусити, як при роботі з PInvoke. Ви не хочете розпоряджатися тим, на що API все ще посилається.
MusiGenesis

3
Правильно реалізувати IDisposable дуже важко і зрозуміти навіть найкращі поради, які я знайшов щодо цього (.NET Framework Guidelines), можуть заплутати застосувати, поки ви нарешті не "отримаєте його".
Дивовижний

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

19

Реалізуємо масиви IList

Але не реалізовуйте це. Коли ви телефонуєте "Додати", він повідомляє, що це не працює. То чому ж клас реалізує інтерфейс, коли він не може його підтримувати?

Компілює, але не працює:

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

У нас це питання багато, тому що серіалізатор (WCF) перетворює всі ILists в масиви, і ми отримуємо помилки під час виконання.


8
IMHO, проблема полягає в тому, що Microsoft не має достатньо інтерфейсів, визначених для колекцій. IMHO, він повинен мати iEnumerable, iMultipassEnumerable (підтримує скидання, і гарантує збіг декількох пропусків), iLiveEnumerable (мав би частково визначену семантику, якщо колекція змінюється під час перерахунку - зміни можуть бути або не відображатись у перерахунку, але не повинні спричиняти фальшиві результати або винятки), iReadIndexable, iReadWriteIndexable і т. д. Оскільки інтерфейси можуть "успадкувати" інші інтерфейси, це не додало б багато зайвої роботи, якщо така є (це дозволить зберегти NotImplemented заглушки).
supercat

@supercat, це було б заплутано як пекло для початківців та певних давніх кодерів. Я думаю, що .NET-колекції та їхні інтерфейси надзвичайно елегантні. Але я ціную вашу смиренність. ;)
Йорданія

@ Джордан: Після написання вищесказаного я вирішив, що кращим підходом було б мати як IEnumerable<T>і IEnumerator<T>підтримувати Featuresвластивість, так і деякі "необов'язкові" методи, корисність яких визначатиметься тим, про що повідомляють "Особливості". Але я стою під своїм головним моментом, який полягає в тому, що є випадки, коли код, який отримує IEnumerable<T>заповіт, потребує більш сильних обіцянок, ніж IEnumerable<T>передбачено. Телефонування ToListдасть IEnumerable<T>змогу виконувати такі обіцянки, але у багатьох випадках буде марно дорогою. Я б сказав, що повинно бути ...
supercat

... засіб, за допомогою якого код, що отримує, IEnumerable<T>може зробити копію вмісту, якщо це потрібно, але може утриматися від цього марно.
supercat

Ваш варіант абсолютно не читабельний. Коли я бачу 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" легше змішувати зі зміненою шкалою. Із загальною для-циклів змінна індекс очевидно однакова для кожної ітерації.
Мікко Рантанен

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 року стають химерними через зміни календаря, дні, повністю пропущені тощо. Більшість RDBM мають деякий відрізний момент. Це повинно дати тобі вихідну точку: ancestry.com/learn/library/article.aspx?article=3358
NotMe

11
Крім того, дата - 1753 р. Що було майже вперше, коли у нас є безперервний календар без пропускання дат. SQL 2008 представив тип дати Date and datetime2, який може приймати дати з 01.01.01 по 31.12.9999. Однак порівняння дат із використанням цих типів слід розглядати з підозрою, якщо ви дійсно порівнюєте дати до 1753 року.
NotMe

О, так, 1753, виправлено, спасибі.
Shaul Behr

Чи справді є сенс робити порівняння дат із такими датами? Я маю на увазі, що для History Channel це має багато сенсу, але я не бачу, щоб я хотів дізнатися точний день тижня, коли була відкрита Америка.
Каміло Мартін

5
Через Вікіпедію в День Джуліана ви можете знайти 13-рядкову базову програму CALJD.BAS, опубліковану в 1984 році, яка може робити розрахунки дат приблизно до 5000 року до н.е. "такі системи, як SQL2008, повинні робити гірше. Вас може не зацікавити правильне представлення дат у 15 столітті, але інші можуть, і наше програмне забезпечення повинно це впоратися без помилок. Інша проблема - це високосні секунди. . .
Roland

18

Спритний Linq Caching Gotcha

Подивитися моє запитання, яке призвело до цього відкриття, і блогер який виявив проблему.

Коротше кажучи, DataContext зберігає кеш усіх об'єктів Linq-to-Sql, які ви коли-небудь завантажували. Якщо хтось інший внесе зміни до запису, який ви раніше завантажили, ви не зможете отримати останні дані, навіть якщо ви явно перезавантажите запис!

Це пов’язано з властивістю, що називається ObjectTrackingEnabledв DataContext, яка за замовчуванням відповідає істині. Якщо ви встановите це властивість на значення false, запис буде завантажуватися заново кожного разу ... АЛЕ ... ви не зможете зберегти жодних змін до цього запису за допомогою SubmitChanges ().

ГОТЧА!


Iv щойно провів півтора дня (і куча волосся!), Переслідуючи цю КРУГУ ...
хірургічний кодер

Це називається конфліктом одночасності, і це все ще є ґаздом сьогодні, хоча зараз існують певні шляхи, хоча вони, як правило, трохи важкі. DataContext був кошмаром. O_o
Йорданія

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 - Danny Pflughoeft

3
Мені не подобається реалізація подій .net. Підписку на події слід обробляти шляхом виклику методу, який додає підписку та повертає ідентифікатор, який, коли Dispose'd, видалить підписку. Немає необхідності в спеціальній конструкції, що поєднує метод "додавання" та "видалення", семантика якої може бути дещо хиткою, особливо якщо спробувати додати та пізніше видалити делегат багатоадресної передачі (наприклад, Додати "В", а потім "AB", а потім видалити "B" (залишаючи "BA") і "AB" (все ще залишає "BA"). На жаль,
supercat

@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()знищував би всі підписки.
supercat

@ Ark-kun: Необхідність зберігати об’єкти, які інкапсулюють зовнішні підписки, може здатися неприємністю, але щодо підписок як сутностей це дозволить об'єднати їх з типом, який може забезпечити їх очищення, що інакше дуже важко.
supercat

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
radarbob

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. Мовляв, автоматичне завантаження? Можна було легко реалізувати це. Це виглядає в основному через відсутність знань програміста, я не думаю, що в поточній поведінці нічого поганого.
Eldritch Conundrum

13

Щойно знайшов дивного, який на деякий час застряг у налагодженні:

Ви можете збільшити null для нульового int, не кидаючи виняток, і значення залишається null.

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 - Danny Pflughoeft
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.