Испытания и невзгоды при создании широко совместимой библиотеки

Около десяти лет назад я написал библиотеку разбора CSV для C#. Моей компании в то время был передан проект, который включал в себя полный дамп данных об использовании AppStore за удивительно длительный период времени. Мы получили десятки файлов, каждый объемом более 30 гигабайт данных в сжатом формате CSV.

Задача состояла в том, чтобы переварить этот большой набор данных и составить полезную сводку на основе некоторых значимых критериев. Я написал небольшую потоковую CSV-программу, которая могла открывать эти файлы, анализировать небольшой объем данных за раз, агрегировать результаты и повторяться по требованию по мере уточнения нашего анализа.

Превратить его в библиотеку

После завершения проекта я решил открыть исходный код CSV. Я подумал, что будет интересной задачей определить, как взять небольшой интересный фрагмент кода и попытаться сделать его максимально полезным.

Я хотел решить несколько задач:

  • Опубликовать библиотеку на NuGet
  • Поддерживайте как можно больше Версий DotNet Framework
  • Практически не использовать зависимости
  • Научите себя разработке библиотек по пути

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

Платформы и версии Dot Net

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

какой я должен выбрать?

  • DotNet 1.0 и 1.1 (~2002 г.) уже были на пути к выходу, когда я написал свой код. Я решил не поддерживать их.
  • DotNet 2.0 (~ 2005 г.) была первой версией, поддерживающей универсальные классы. Поскольку я действительно хотел, чтобы моя CSV-библиотека могла сериализовать и десериализовать универсальные классы, это было для меня ключевой целью.
  • DotNet 3.0 (~ 2006 г.) и 3.5 (~ 2007 г.) в основном представили новые платформы, такие как WCF / WPF, и такие удобства кодирования, как LINQ. К счастью, оба они обратно совместимы с 2.0, поэтому нет необходимости нацеливаться на них напрямую.
  • В DotNet 4.0 (~ 2010 г.) появилась новая языковая среда выполнения, а в версии 4.5 (~ 2012 г.) — асинхронное поведение. Я хотел иметь версию, поддерживающую эту среду выполнения, но я решил реализовать библиотеку CSV с помощью итератора на основе yield. В то время я этого не осознавал, но такое неасинхронное поведение не позволяло мне использовать async/await.

К сожалению, здесь дела пошли наперекосяк. DotNet начал внедрять необычные идеи, такие как «клиентские» версии фреймворка, «портативные», Mono и DotNetCore. Каждая из них что-то реализовывала, что-то не реализовывала и вообще была несовместима со всем остальным. Дела шли не очень хорошо.

Введите DotNetStandard

Очевидно, кто-то получил сообщение и начал исправлять ситуацию. После непростых первых нескольких выпусков — 1.0 и 1.3 были заведомо несовместимы — «NetStandard» стал достаточно прочным, чтобы его можно было использовать. Я выбрал версию 2.0 как текущую платформу, которая лучше всего подходит для версий DotNetCore как для Mac, так и для ПК.

С этим я решил, что могу создать библиотеку, поддерживающую следующие фреймворки:

  • Библиотека DotNet 2.0, поддерживающая версии 2.0, 3.0 и 3.5.
  • Библиотека DotNet 4.0, поддерживающая все версии 4.x платформы.
  • Библиотека NetStandard 2.0, поддерживающая DotNetCore и изначально работающая на Mac и ПК.

Что потребуется для создания единого набора кода, поддерживающего все три?

Несовместимость версий

Первая странность, которую я обнаружил, заключается в том, что DotNet 2.0 по умолчанию не поддерживает методы расширения. Мне очень понравился синтаксис методов расширения. Я хотел, чтобы пользователь мог сериализовать массив в CSV напрямую, вызвав array.ToCsvString()

Интересно, что добавление крошечного фрагмента кода в мою версию DotNet 2.0 решило проблему. Я добавил #define в свой проект компилятора и добавил этот фрагмент кода для конкретной версии DotNet 2.0:

#if NET20
namespace System.Runtime.CompilerServices
{    
    [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]    
    public sealed class ExtensionAttribute : Attribute 
    {
    }
}
#endif

Однако DataTables не так удачливы. Реализация DataTables еще не доступна по умолчанию в NetStandard 2.0, поэтому моей библиотеке придется обойтись без них. Я переместил весь код DataTable в отдельный файл и использовал операторы #if, чтобы закомментировать его для NetStandard 2.0.

Можем ли мы создать дополнительную поддержку?

Когда я впервые написал эту библиотеку, асинхронного C# еще не существовало. Однако такое поведение async/await было именно тем, чего я хотел, когда изначально писал библиотеку. Моя программа обработки CSV должна быть способна захватить фрагмент текста, как только система ввода-вывода сможет произвести достаточно байтов для захвата строки.

В идеале я бы заменил свою систему IEnumerable, основанную на доходности, на систему, которая работает примерно так:

using (var r = new AsyncCSVReader(stream)) {
    while (!r.EndOfFile()) {
        var line = await r.ReadLine();
        ... do work on line ...
    }
}

Точно так же я хотел бы иметь возможность поддерживать библиотеки NetStandard более низкого уровня. У меня были некоторые запутанные попытки настроить таргетинг на NetStandard 1.3 и 1.6, но было бы интересно попробовать. Чтобы добиться большего прогресса, мне нужно ответить на следующие вопросы:

  • Если бы я начал использовать потоковую асинхронность, смог бы я точно определить условие конца файла?
  • Что произойдет, если я попытаюсь прочитать строку в асинхронной функции ReadLine() и достигну поврежденных данных или конца файла?

Это потребует дополнительных исследований. Я поделюсь больше в следующей статье.