Как стать автором
Обновить

Комментарии 16

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Хм. То есть в С# в последнем примере нельзя просто вернуть stream из функции?

А какой смысл возвращать поток, если вы в него пишете что-то?

НЛО прилетело и опубликовало эту надпись здесь

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

Понимаете, строка — это что-то, что вы уже аллоцировали. Буфер — это то, что аллоцировал вызывающий, а вы туда записали (предпочтительно, без своей аллокации).


Аналогично и в случае с потоком: если вы его возвращаете с содержимым, значит, вы выделили на это содержимое память (за исключением того сложного случая, когда вы создаете специальный поток, который умеет читать из вашего объекта), а потом ваш потребитель создает новый поток, куда он будет писать (например, в файл). Зачем тогда вы выделяли память на своей стороне, если вы могли просто принять поток, куда писать?

НЛО прилетело и опубликовало эту надпись здесь
На С++ я могу написать поток, который не аллоцирует никакой памяти, а отдаёт данные в методе чтения

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


сейчас принято аллоцировать строку и возвращать её, а не писать в принимаемый буфер

А это, кстати, не обязательно так. Есть много интерфейсов (например, JSON-сериализация), которые пишут строку в TextWriter, а не возвращают ее. Ну и да, есть хелперный метод, который создает TextWriter, отдает в сериализатор, потом забирает из него строку.

На С++ я могу написать поток, который не аллоцирует никакой памяти, а отдаёт данные в методе чтения, можно перегрузить присваивание и т.д., т.е. семантика значения.

Проблема в том, что при таком подходе формировать результат придётся асинхронно, даже если сам результат нужен синхронный. То есть понадобится использовать либо конечные автоматы, либо сопрограмму. В обоих случаях возрастает сложность почти на пустом месте. И к тому же резко ограничиваются доступные библиотеки.


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

Если вам это кажется нонсенсом

Неудачная калька с английского. Лучше было бы "если вы не понимаете, о идет речь" или что-то в это роде.


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

Что-то тут не так.


public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

Лучше так не делать. Если в коде будет вызываться этот метод, замокать его будет практически нереально — да, реализацию IExportService можно будет подменить, чтобы в файл ничего не писалось, но сам файл на диске будет создаваться, со всеми вытекающими недостатками.

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

Кстати, да. Я все пытался понять, что меня смущает, спасибо, что вы указали: я как-то привык делать методы-расширения без собственных побочных эффектов, потому что это проще тестировать.

Минусы экстеншнов, как и любой статики — фиг замокаешь при написании тестов, стоит помнить об этом.

А как же Fody? Если мне не изменякт память, у него есть плагины, позволяющие подменять вызов статических методов.
Можно еще harmony использовать (он хотя бы не изменяет il code), но это все равно не то, что хочется делать в тестах.
Полагаю, нужно разграничивать цели, для которых можно использовать такой подход.

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

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

Впрочем, и для методов расширения можно реализовать механизмы, которые позволят переопределить логику работы в юнит-тестах, если внутри них обращаться к синглтону, который и содержит логику их работы, и может быть подменён при запуске тестов.
«Добавление методов к перечислениям»
Можно пожалуйста не надо, особенно если это ваше перечисление. Лучше написать класс / структуру с нормальными методами без публичного конструктора и несколькими статическими риоднли членами из этого типа. Работает все точно так же как перечисление но без попыток натянуть сову на глобус.
Ну и вообще если вам надо добавить какую либо логику в само перечисление, значит вы делаете что-то не так.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий