Как перенести мое мышление с C ++ на C #

12

Я опытный разработчик C ++, знаю язык очень подробно и интенсивно использую некоторые его особенности. Также я знаю принципы OOD и шаблоны проектирования. Сейчас я изучаю C #, но не могу избавиться от ощущения, что не могу избавиться от мышления C ++. Я так сильно привязался к сильным сторонам C ++, что не могу жить без некоторых функций. И я не могу найти какие-либо хорошие обходные пути или замены для них в C #.

Какие хорошие практики , шаблоны проектирования , идиомы , которые отличаются от C # с точки зрения C ++, вы можете предложить? Как получить идеальный дизайн C ++, не выглядящий немым в C #?

В частности, я не могу найти хороший C # -ish способ иметь дело (последние примеры):

  • Управление временем жизни ресурсов, которые требуют детерминированной очистки (например, файлов). Это легко иметь под usingрукой, но как правильно его использовать, когда право собственности на ресурс передается [... между потоками]? В C ++ я просто использовал бы общие указатели и позволял бы позаботиться о «сборке мусора» как раз в нужное время.
  • Постоянно борется с переопределением функций для конкретных обобщений (мне нравятся такие вещи, как частичная специализация шаблонов в C ++). Должен ли я просто отказаться от любых попыток сделать какое-либо общее программирование на C #? Может быть, дженерики ограничены по назначению, и это не C # -ish, чтобы использовать их, за исключением конкретной области проблем?
  • Макро-подобная функциональность. Хотя в целом это плохая идея, для некоторых областей проблем другого обходного пути нет (например, условная оценка оператора, например, для журналов, которые должны идти только в версии Debug). Отсутствие их означает, что мне нужно поставить больше if (condition) {...}шаблонов, и это все еще не равно с точки зрения запуска побочных эффектов.
Андрей
источник
№ 1 для меня сложный вопрос, хотелось бы узнать ответ сам. # 2 - не могли бы вы привести простой пример кода C ++ и объяснить, зачем вам это нужно? Для # 3 - использовать C#для генерации другого кода. Вы можете прочитать CSVили, XMLили что вы подали в качестве ввода и сгенерировать C#или SQLфайл. Это может быть более мощным, чем использование функциональных макросов.
Работа
Что касается макроподобной функциональности, что конкретно вы пытаетесь сделать? Я обнаружил, что обычно существует языковая конструкция C # для обработки того, что я сделал бы с макросом в C ++. Также ознакомьтесь с директивами препроцессора C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Многие вещи, выполняемые с помощью макросов в C ++, могут быть выполнены с помощью отражения в C #.
Себастьян Редл
Это одна из моих любимых мозолей, когда кто-то говорит: «Правильный инструмент для правильной работы - выбирайте язык в зависимости от того, что вам нужно». Для больших программ на языках общего назначения это бесполезное, бесполезное утверждение. Тем не менее, то, что C ++ лучше, чем почти любой другой язык, это детерминистическое управление ресурсами. Является ли детерминированное управление ресурсами основной функцией вашей программы или к чему-то, к чему вы привыкли? Если это не важная функция пользователя, вы можете быть слишком сосредоточены на этом.
Бен

Ответы:

16

По моему опыту, нет книги, чтобы сделать это. Форумы помогают с идиомами и лучшими практиками, но, в конце концов, вам придется потратить время. Практикуйтесь, решая ряд различных проблем в C #. Посмотрите на то, что было трудно / неловко, и постарайтесь сделать их менее трудными и менее неловкими в следующий раз. Для меня этот процесс занял около месяца, прежде чем я «получил его».

Самая важная вещь, на которой нужно сосредоточиться - это отсутствие управления памятью. В C ++ это влияет на каждое ваше дизайнерское решение, но в C # это контрпродуктивно. В C # вы часто хотите игнорировать смерть или другие изменения состояния ваших объектов. Такая ассоциация добавляет ненужную связь.

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

Как получить идеальный дизайн C ++, не выглядящий немым в C #?

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

Но в ответ на конкретные сценарии:

Совместное использование неуправляемых ресурсов - это была плохая идея в C ++, и такая же плохая идея в C #. Если у вас есть файл, откройте его, используйте и закройте. Разделение его между потоками просто вызывает проблемы, и если только один поток действительно использует его, то пусть этот поток открывает / закрывает / закрывает. Если у вас действительно по какой-то причине действительно долгоживущий файл, то создайте класс для его представления и дайте приложению возможность управлять этим по мере необходимости (закрытие перед выходом, привязка близко к событиям, связанным с закрытием приложения, и т. Д.)

Частичная специализация шаблонов - вам здесь не повезло, хотя чем больше я использовал C #, тем больше я нахожу использование шаблонов в C ++, чтобы попасть в YAGNI. Зачем делать что-то общее, когда T всегда int? Другие сценарии лучше всего решаются в C # путем либерального применения интерфейсов. Вам не нужны универсальные шаблоны, когда интерфейс или тип делегата могут строго указывать то, что вы хотите изменить.

Условный препроцессор - C # имеет #if, офигеть. Более того, он имеет условные атрибуты , которые просто удаляют эту функцию из кода, если символ компилятора не определен.

Telastyn
источник
Что касается управления ресурсами, в C # довольно часто иметь классы менеджера (которые могут быть статическими или динамическими).
Rwong
Что касается общих ресурсов: управляемые ресурсы просты в C # (если условия гонки не имеют значения), неуправляемые гораздо более противные.
Дедупликатор
@rwong - обычное дело, но классы менеджеров по-прежнему являются антишаблоном, которого следует избегать.
Теластин
Что касается управления памятью, я обнаружил, что переход на C # усложнил это, особенно в начале. Я думаю, что вы должны быть более осведомленными, чем в C ++. В C ++ вы точно знаете, когда и где каждый объект размещается, освобождается и на него ссылаются. В C # это все скрыто от вас. Много раз в C # вы даже не понимаете, что ссылка на объект была получена и память начинает течь. Получайте удовольствие от отслеживания утечек памяти в C #, которые обычно легко понять в C ++. Конечно, с опытом вы узнаете эти нюансы C #, поэтому они становятся менее серьезной проблемой.
Данк
4

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

Находясь в той же лодке, что и вы - разработчик C ++, занимающийся C # atm. - отсутствие поддержки модели Ownership Transfer (файлы, db-соединения, ...) в типах / сигнатурах C # меня очень раздражало, но я должен признать, что это работает вполне нормально, чтобы «просто» полагаться на соглашение и API документы, чтобы получить это право.

  • Используйте IDisposableдля выполнения детерминированной очистки, если это необходимо.
  • Когда вам нужно передать право собственности на an IDisposable, просто убедитесь, что соответствующий API понятен, и не используйте usingв этом случае, потому что usingнельзя, так сказать, «отменить».

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

Мартин Ба
источник
2

Вы упомянули две специфические особенности C ++. Я буду обсуждать RAII:

Обычно это не применимо, поскольку C # является сборщиком мусора. Наиболее близкой конструкцией C #, вероятно, является IDisposableинтерфейс, используемый следующим образом:

using (FileStream fs = File.OpenRead(@"c:\path\filename.txt"))
{
   //Do stuff with the file.
} //File will be closed here.

Не следует ожидать частой реализации IDisposable(и это немного продвинуто), так как это не требуется для управляемых ресурсов. Тем не менее, вы должны использовать usingконструкцию (или вызывать Disposeсебя, если usingэто невозможно) для чего-либо реализующего IDisposable. Даже если вы ошибетесь, хорошо спроектированные классы C # попытаются справиться с этим за вас (используя финализаторы). Однако в языке с мусором шаблон RAII работает не полностью (поскольку сбор недетерминирован), поэтому очистка вашего ресурса будет отложена (иногда катастрофически).

Брайан
источник
RAII моя самая большая проблема на самом деле. Скажем, у меня есть ресурс (скажем, файл), который создается одним потоком и передается другому. Как я могу убедиться, что он удаляется в определенное время, когда этот поток больше не нужен? Конечно, я мог бы вызвать Dispose, но тогда это выглядит намного страшнее, чем общие указатели в C ++. В этом нет ничего от RAII.
Андрей
@Andrew То, что вы описываете, похоже, подразумевает передачу права собственности, поэтому теперь второй Disposing()файл отвечает за файл и, вероятно, должен его использовать using.
svick
@ Андрей - В общем, вы не должны этого делать. Вместо этого вы должны указать потоку, как получить файл и предоставить ему право собственности на время жизни.
Теластин
1
@Telastyn Означает ли это, что передача прав владения управляемым ресурсом является более серьезной проблемой в C #, чем в C ++? Довольно удивительно.
Андрей
6
@ Андрей: Позвольте мне подвести итог: в C ++ вы получаете единообразное, полуавтоматическое управление всеми ресурсами. В C # вы получаете полностью автоматическое управление памятью и полностью ручное управление всем остальным. Там нет реального изменения этого, так что ваш единственный реальный вопрос не «как мне это исправить?», А только «как мне привыкнуть к этому?»
Джерри Гроб
2

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

Лучше не думать о C # как о «C ++ с более приятным синтаксисом». Это разные языки, которые требуют разных подходов в вашем мышлении. C ++ заставляет вас постоянно думать о том, что будет делать процессор и память. C # не так. C # специально разработан таким образом, чтобы вы не думали о процессоре и памяти, а вместо этого думали о бизнес-сфере, для которой вы пишете .

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

Я думаю, что лучший способ перенастроить C ++ на C # - это попробовать подойти к кодированию с другой точки зрения. Сначала это будет сложно, потому что с годами вы будете полностью привыкнуть использовать поток «что делает процессор и память» в вашем мозгу для фильтрации кода, который вы пишете. Но в C # вместо этого вы должны думать об отношениях между объектами в бизнес-сфере. «Что я хочу сделать», а не «что делает компьютер».

Если вы хотите что-то сделать со всем в списке объектов, вместо записи цикла for в списке, создайте метод, который принимает IEnumerable<MyObject>и использует foreachцикл.

Ваши конкретные примеры:

Управление временем жизни ресурсов, которые требуют детерминированной очистки (например, файлов). Это легко использовать в руках, но как правильно его использовать, когда право собственности на ресурс передается [... между потоками]? В C ++ я просто использовал бы общие указатели и позволял бы позаботиться о «сборке мусора» как раз в нужное время.

Вы никогда не должны делать это в C # (особенно между потоками). Если вам нужно что-то сделать с файлом, делайте это в одном месте одновременно. Это нормально (и действительно хорошая практика) написать класс-оболочку, который управляет неуправляемым ресурсом, который передается между вашими классами, но не пытайтесь передавать файл между потоками и иметь отдельные классы для записи / чтения / закрытия / открытия его. Не делитесь правами собственности на неуправляемые ресурсы. Используйте Disposeобразец, чтобы иметь дело с очисткой.

Постоянно борется с переопределением функций для конкретных обобщений (мне нравятся такие вещи, как частичная специализация шаблонов в C ++). Должен ли я просто отказаться от любых попыток сделать какое-либо общее программирование на C #? Может быть, дженерики ограничены по назначению, и это не C # -ish, чтобы использовать их, за исключением конкретной области проблем?

Обобщения в C # разработаны так, чтобы быть обобщенными. Специализации дженериков должны обрабатываться производными классами. Зачем List<T>вести себя по-другому, если это List<int>или List<string>? Все операции List<T>являются общими и применимы к любому List<T> . Если вы хотите изменить поведение .Addметода в a List<string>, то создайте производный класс MySpecializedStringCollection : List<string>или составной класс, MySpecializedStringCollection : IList<string>который использует обобщенный тип для внутреннего использования, но работает по-другому. Это поможет вам не нарушать принцип подстановки Лискова и по-королевски подставлять других, кто использует ваш класс.

Макро-подобная функциональность. Хотя в целом это плохая идея, для некоторых областей проблем другого обходного пути нет (например, условная оценка оператора, например, для журналов, которые должны идти только в версии Debug). Отсутствие их означает, что мне нужно поставить больше, если (условие) {...} шаблон, и это все еще не равно с точки зрения запуска побочных эффектов.

Как уже говорили другие, вы можете использовать команды препроцессора для этого. Атрибуты еще лучше. В общем, атрибуты - лучший способ справиться с вещами, которые не являются «основной» функциональностью для класса.

Короче говоря, при написании C # имейте в виду, что вы должны полностью думать о бизнес-сфере, а не о процессоре и памяти. Вы можете оптимизировать позже , если это необходимо , но ваш код должен отражать отношения между деловыми принципами , которые вы пытаетесь картой.

Стивен
источник
3
WRT. Передача ресурсов: «вы никогда не должны этого делать», это не значит, что ИМХО. Зачастую очень хороший дизайн предполагает перенос IDisposable( не необработанного ресурса) из одного класса в другой: просто посмотрите API StreamWriter, где это имеет смысл для Dispose()переданного потока. И здесь есть дыра в языке C # - вы не можете выразить это в системе типов.
Мартин Ба
2
«Один из примеров того, что я видел, состоит в том, что многие разработчики на C ++ хотели бы использовать циклы, потому что они быстрее, чем foreach». Это плохая идея и в C ++. Абстракция с нулевой стоимостью - великая сила C ++, и в большинстве случаев foreach или другие алгоритмы не должны быть медленнее, чем рукописный цикл for.
Себастьян Редл
1

Для меня больше всего отличалась тенденция к новому всему . Если вам нужен объект, забудьте о его передаче в подпрограмму и обменивайтесь базовыми данными, кажется, что шаблон C # предназначен просто для создания нового объекта. Это, в общем-то, похоже на C #. Сначала этот шаблон казался очень неэффективным, но я думаю, что создание большинства объектов в C # - быстрая операция.

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

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

Вы очень быстро поймете это, хотя C # практически не отличается от VB, поэтому вы можете рассматривать его как тот же инструмент RAD, что и он (если он помогает думать о C # как VB.NET, тогда сделайте так, синтаксис немного другие, и мои старые времена использования VB привели меня к правильному мышлению для разработки RAD). Единственным сложным моментом является определение того, какую часть огромных библиотек .net вам следует использовать. Google, безусловно, ваш друг здесь!

gbjbaanb
источник
1
Передача объекта в подпрограмму для совместного использования лежащих в основе данных прекрасно работает в C #, по крайней мере с точки зрения синтаксиса.
Брайан