Опыт использования AutoFixture для генерации gRPC сообщений

также размещено здесь

Вступление

Думаю, большинство читателей согласится, что автоматизированное тестирование — полезный, а во многих областях даже необходимый, этап создания программ. А так как программисты — народ ленивый, то и инструментов, облегчающих этот этап существует немало. Одним из таких инструментов является AutoFixture — средство для генерации тестовых экземпляров. Этот инструмент уже не раз упомянался на Хабре, например тут. Далее я расскажу, с какой проблемой столкнулся в попытке применить AutoFixture в своей работе и как решил эту проблему.
Вкратце напомню, как выглядит использование AutoFixture на практике.

using AutoFixture;

var fixture = new Fixture();

var intValue = fixture.Create<int>();
Console.WriteLine(intValue);

var complexType = fixture.Create<ComplexType>();
Console.WriteLine(complexType);

var collection = fixture.Create<List<ComplexType>>();
Console.WriteLine(string.Join(", ", collection));

record ComplexType(int IntValue, string StringValue);

Как видно из приведённого выше примера, инструмент способен создавать и встроенные типы, и пользовательские, и коллекции произвольных типов. Главное — чтобы у них был доступен конструктор, а типы его параметров, в свою очередь подходили под эти же условия.

Проблема

Мне в работе понадобилось создавать тестовые данные типов gRPC сообщений. Сами эти типы генерируются автоматически по proto-файлам.
Для начала, давайте создадим экземляр сообщения для такого контракта:

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
using AutoFixture;
using AutoFixtureWithGrpc;

var fixture = new Fixture();

var message = fixture.Create<HelloRequest>();
Console.WriteLine(message);

Пока всё работает: экземпляр создаётся, свойство инициализируется непустой строкой, класс!
Попробуем добавить поле с атрибутом repeated. По спецификации protobuf такие поля могут иметь любое количество элементов.

message HelloRequest {
  string name = 1;
  repeated int32 lucky_numbers = 2;
}

Бам!!! Что случилось? Коллекция LuckyNumbers в экземпляре сгенерированного типа оказывается пустой. Дело в том, что AutoFixture по умолчанию инициализирует экземпляр типа, вызывая его конструктор, а затем все доступные сеттеры свойств. А repeated-поля контракта становятся свойствами, у которых есть только геттер, а сеттера нет:

public sealed partial class HelloRequest : pb::IMessage<HelloRequest>
{
    // .. часть кода пропущена для краткости
    public HelloRequest() { }

    public pbc::RepeatedField<int> LuckyNumbers {
      get { /* ... */ }
    }
}

Из кода видно, что у свойства LuckyNumbers отсутсвует доступный сеттер, поэтому-то AutoFixture и не смог заполнить коллекцию элементами!
Быстрое «гугление» подсказало, что можно покрутить настройки AutoFixture таким образом:

var fixture = new Fixture();
fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());

Такая настройка должна сообщить инструменту, что нужно заполнять свойства-коллекции даже если у них отсутствует доступный сеттер. Лишь бы был геттер, да метод Add у коллекции.
Пробуем:

fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());
var message = fixture.Create<HelloRequest>();
Console.WriteLine(message.LuckyNumbers.Count);

и получаю Бам №2!!! :

System.Reflection.AmbiguousMatchException: Ambiguous match found.

Тут я, признаюсь, немного приуныл. Затем решил проверить, в чём же дело: в AutoFixture или в сгенерированном по контракту коде. Для этого я набросал небольшой класс с таким же свойством без сеттера с той лишь разницей, что в этот раз типом коллекции был простой List<int>.

class Investigation
{
    private readonly List<int> _values = new();
    public List<int> Ints => _values;
}
fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());
var message = fixture.Create<Investigation>();
Console.WriteLine(message.Ints.Count);

На этот раз никакого исключения не вылетело, в коллеции лежали элементы, как и положено. Подозрение, что в прошлый раз исключение появилось из-за особенностей класса RepeatedField<T> всё крепло.
Я зарылся в отладчик, пытаясь понять, что же такого неоднозначного (ambiguous) было в RepeatedField, чего не было у List. В отладчике ставлю точку останова на исключение System.Reflection.AmbiguousMatchException.
Довольно быстро выяснилось, что исключение происходит в методе InstanceMethodQuery.SelectMethods. Благо, исходный код инструмента открыт, привожу текст метода:

public IEnumerable<IMethod> SelectMethods(Type type = default)
{
    var method = this.Owner.GetType().GetTypeInfo().GetMethod(this.MethodName);

    return method == null
        ? new IMethod[0]
        : new IMethod[] { new InstanceMethod(method, this.Owner) };
}

И при этом MethodName имеет значение «Add». Обозреватель сборок в Rider-е показал (см. картинку), что у типа RepeaterField есть два публичных метода Add: один для одиночного элемента, другой — для их последовательности. Поэтому-то AutoFixture не мог выбрать, какой именно метод ему нужен и падал с ошибкой. А если точнее, то падал метод GetMethod в кишках дотнетовского рантайма.

Решение

Ну что же, причина проблемы стала ясна. Оставалось придумать решение. Я решил добавить в AutoFixture дополнительную настройку, позволяющую инициализировать именно экземпляры типа RepeatedField<T>. По счастью, у этого злополучного типа оказался метод AddRange, который я и собрался использовать для наполнения коллекции.

Я решил идти проверенным методом copy-paste и продублировать код ReadonlyCollectionPropertiesBehavior, меняя его лишь по необходимости. Оказалось, что менять придётся совсем немного: поиск подходящего метода инициализации (того самого AddRange) и подготовку параметров для него. Потому что если ReadonlyCollectionPropertiesBehavior заполнял коллекцию поэлементно, вызывая Add, то мне предстояло сперва подготовить последовательность элементов, и лишь затем единожды вызвать AddRange, передав её всю целиком.

Тут уже никаких сложностей не осталось. Готовое решение можно найти в моём репозитории на гитхабе.

Я благодарен авторам AutoFixture за такой полезный инструмент и призываю всех шарпистов рассмотреть возможность использовать его в своей практике.

Следует ли делать асинхронную обёртку для синхронного метода?

перевод

В последнее время меня несколько раз спрашивали похожие вопросы, которые можно объединить как «асинхронная обёртка поверх синхронного кода»:

В моей библиотеке есть метод public T Foo(). Я рассматриваю возможность предоставить асинхронный метод, который будет просто оборачивать синхронный, то есть public Task<T> FooAsync() { return Task.Run(() => Foo()); }. Вы бы порекомендовали такой подход?

Если отвечать коротко — нет. Но тогда не получилось бы хорошей заметки в блоге. Так что ниже мой более развёрнутый ответ…

Зачем асинхронный?

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

Масштабируемость

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

В качестве примера посмотрите на синхронный метод Sleep, который ничего не делает в течение N миллисекунд:

public void Sleep(int millisecondsTimeout)
{
    Thread.Sleep(millisecondsTimeout);
}

Теперь, нам необходимо создать асинхронную версию этого, то есть такую, чтобы возвращенный Task не завершался в течение N миллисекунд. Вот одна из возможных реализаций, мы просто обернём Task.Run вокруг Sleep и создадим SleepAsync:

public Task SleepAsync(int millisecondsTimeout)
{
    return Task.Run(() => Sleep(millisecondsTimeout));
}

а вот другой, который не использует Sleep, и в итоге расходует меньше ресурсов:

public Task SleepAsync(int millisecondsTimeout)
{
    TaskCompletionSource<bool> tcs = null;
    var t = new Timer(delegate { tcs.TrySetResult(true); }, null, –1, -1);
    tcs = new TaskCompletionSource<bool>(t);
    t.Change(millisecondsTimeout, -1);
    return tcs.Task;
}

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

Разгрузка

Возможность вызвать синхронный код асинхронно может быть очень полезна для отзывчивости, так как вы переносите выполнение длительных операций на другие потоки. Тут речь идёт не о том, как много ресурсов вы потребляете, скорее о том, что это за ресурсы. Например, в UI приложениях специальный поток занимается обработкой сообщений пользовательского интерфейса и поэтому он более ценен для пользовательского опыта, чем другие потоки, например из пула. Следовательно, асинхронно вызывая метод из потока интерфейса в потоке из пула позволяет нам тратить меньше ценного ресурса. Такой тип вызовов не требует модификации реализации той операции, так как можно достаточно эффективно использовать обёртку.

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

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

При чём здесь мой вопрос?

Давайте вернёмся к исходному вопросу: следует ли нам предоставлять асинхронную точку вызова для метода, который в действительности синхронный? Позиция, которую мы заняли в Task-based Async Pattern, — твёрдое «нет».

Заметьте, что в моих предыдущих рассуждениях о масштабируемости и разгрузке, я сказал, что путь увеличения масштабируемости — это изменение реализации, тогда как разгрузку можно выполнить с помощью обёртки, не требующей изменения реализации. Это — ключ. Оборачивание синхронного метода в простой асинхронный фасад не улучшает масштабируемость, и в этом случае, предоставляя клиентам только синхронный метод вы получаете некоторые преимущества:

  • Внешняя поверхность вашей библиотеки уменьшается. Для вас это означает уменьшение стоимости (разработки, тестирования, поддержки, документирования и т.д.). Также это значит, что пользователь не стоит перед сложностями выбора. В то время как некоторый выбор обычно это хорошая вещь, слишком много выбора ведёт к потере продуктивности. Если бы я как пользователь постоянно сталкивался и с синхронным и с асинхронным методом для каждой операции, я постоянно вынужден был бы оценивать, какой из двух в каждом случае следует выбрать.
  • Ваши пользователи будут знать, где именно можно получить преимущества от масштабируемости, потому что тогда только масштабируемые API будут предоставлять асинхронные вызовы.
  • Выбор какой вызов сделать: синхронный или асинхронный остаётся за разработчиком. Асинхронная обёртка над синхронными методами имеет накладные расходы (создание объектов чтобы представлять операцию, переключения контекста, синхронизация очередей и т.д.). Если, к примеру, ваш клиент создаёт высоко-нагруженное серверное приложение, ему не нужны накладные расходы, которые не приносят выгоды, так что они будут просто использовать синхронный метод. Если же и синхронный и асинхронный метод будут представлены в API, разработчик задумается, стоит ли ему использовать асинхронную версию из соображений масштабируемости, но в действительности, будет расплачиваться пропускной способностью системы, тратя ресурсы на накладные расходы и не получая преимуществ масштабируемости.

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

Идея предоставлять асинхронный API для синхронного — это очень скользкая дорожка, в пределе, её применение может вылиться в то, что у каждого метода будет и синхронная и асинхронная форма. Много народу, кто спрашивает меня об этой практике, имеют в виду предоставление асинхронной обёртки для длительных CPU-операций. Желание похвально: помочь с отзывчивостью. Но как уже говорилось, отзывчивость может быть легко обеспечена клиентом API, причём клиент сможет сделать это на нужном уровне разбиения, а не для каждой операции. Более того, определение того, какая операция может быть действительно длительной — на удивление сложная задача. Временные сложности множества методов часто значительно отличаются.

Представьте, к примеру, простой метод вроде Dictionary<TKey, TValue>.Add(TKey, TValue). Это ведь в действительности быстрый метод, верно? Обычно, да, но вспомните, как работает словарь: ему надо вычислить хэш от ключа, чтобы найти позицию, а также проверить на эквивалентность с другими сущностями, находящимися в этой же позиции. Вычисление хэша и проверка эквивалентности могут привести в вызову пользовательского кода и кто знает, какие там будут операции и сколько времени займёт их выполнение. Должен ли каждый метод словаря предоставлять асинхронную обёртку? Это очевидно преувеличенный пример, но есть и попроще, например регулярные выражения. Сложность шаблонов регулярных выражений а также размер и природа входной строки могут существенно повлиять на время выполнения сопоставления, настолько, что сейчас Regex поддерживает опциональный таймаут… должен ли каждый метод класса Regex иметь асинхронный эквивалент? Я надеюсь нет.

Руководство

Всё это был витиеватый способ сказать, что я верю, что в API должны быть только те асинхронные методы, которые дают преимущества при масштабировании (против их синхронных аналогов). Не следует добавлять асинхронные методы в API только из соображений разгрузки: эти преимущества легко достигаются клиентом синхронных методов с помощью функций специально предназначенных для того, чтобы исполнять синхронный код асинхронно, то есть Task.Run.

Конечно, есть исключения из этого, и вы можете обнаружить несколько даже в .NET Framework 4.5.

Например, абстрактный базовый класс Stream предоставляет методы ReadAsync и WriteAsync. В большинстве случаев, производные классы работают с источниками данных не в памяти, а значит включают дисковые или сетевые операции ввода-вывода. Поэтому очень вероятно, что производный класс сможет предоставить реализацию ReadAsync и WriteAsync, которые будут использовать асинхронный ввод-вывод вместо того, чтобы блокировать поток и пользоваться синхронным. Кроме того, нам важна возможность пользоваться этими методами полиморфно, не смотря на реальный тип потока, поэтому мы хотим видеть эти методы в базовом классе. Кроме того базовый класс не знает того, как реализовать базовую реализацию асинхронного ввода-вывода, так что лучшее, что он может сделать, это предоставить асинхронные обёртки для синхронных методов Read и Write (в действительности, ReadAsync и WriteAsync оборачивают вызовы BeginRead/EndRead и BeginWrite/EndWrite соответственно, которые не являются переопределяемыми и которые, в свою очередь оборачивают синхронные Read и Write в эквивалент Task.Run).

Другой пример в том же духе — это TextReader, предоставляющий метод вроде ReadToEndAsync, который в базовом классе просто вызывает Task, чтобы обернуть вызов TextReader.ReadToEnd. Ожидается, тем не менее, что разработчик производного класса переопределит ReadToEndAsync чтобы предоставить более масштабируемую реализацию, например, как у StreamReader-а, который использует Stream.ReadAsync.

Почему Materialize? Приспособления для логирования RX.

перевод

Я написал несколько вспомогательных расширений для логирования того, что происходит в моих RX выражениях. Эти расширения используют метод Materialize, у которого вот такая сигнатура:

public static IObservable<Notification<TSource>>
    Materialize<TSource>(this IObservable<TSource> source);

Materialize проецирует поток значений типа TSource в поток уведомлений типа Notification<TSource>. Уведомления имеют свойство Kind, которое говорит нам было ли оригинальное значение получено через OnNext, OnError или OnCompleted. Они дают вам доступ к оригинальным Value или Exception при необходимости. Вы можете также передать уведомление другому наблюдателю через метод Accept.

Другими словами, он превращает неявные уведомления в явные (то что доктор прописал). Уведомления были неявными потому что метод, вызванный у наблюдателя (OnNext, OnError, OnCompleted) говорит нам неявно что за тип уведомления он доставляет. В то время как Notification, который всегда доставляется в OnNext, явно декларирует тип уведомления через своё свойство Kind.

Кстати, существует и соответствующий метод Dematerialize, который производит обратную операцию. И наконец, материализованные потоки посылают OnCompleted после того, как они отослали уведомление с OnError или OnCompleted. Само по себе они никогда не посылают OnError.

Почему это может быть полезно?

Ну если вы когда-нибудь хотели обработать все события потока единообразным способом, без Materialize вы бы застряли на написании обработчиков каждого из трёх методом. С Materialize вы можете сделать обработку в одном месте.

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

public static IObservable<T> LogToConsole<T>(
    this IObservable<T> source, string key = null)
{
    return Observable.Create<T>(observer =>
        source.Materialize().Subscribe(notification =>
        {
            Console.WriteLine(NotifyToString(notification, key));
            notification.Accept(observer);
        }));
}

Он использует Materialize, чтобы получить поток уведомлений, который затем может быть выведен на консоль вспомогательным методом NotifyToString, показанным ниже. Я использую метод Accept, чтобы передать уведомление наблюдателю. Опциональный параметр key позволяет мне снабдить вывод меткой. Например:

Observable.Range(0, 4).LogToConsole("Range: ").Subscribe();

Выведет:

Range:   OnNext(0)
Range:   OnNext(1)
Range:   OnNext(2)
Range:   OnNext(3)
Range:   OnCompleted()

Обратите внимание, что LogToConsole подписывается «по требованию» (как следует делать всем хорошим операторам RX) и сам нуждается в подписчике, чтобы что-то заработало. Он — побочный эффект, так что имейте в виду, он логгирует КАЖДОГО подписчика.

Для меня довольно обычно экспериментировать с выводом операторов в инструментах вроде LINQPad, так что я написал ещё один метод расширения, чтобы позаботиться и о подписке:

public static IDisposable SubscribeConsoleNotifier<T>(
    this IObservable<T> source, string key = null)
{
    return source.Materialize().Subscribe(
        n => Console.WriteLine(NotifyToString(n, key)));
}

И вот пример, использующий всё вместе:

Observable.Range(0, 4)
          .LogToConsole("Range: ")
          .Where(i => i % 2 == 0)
          .SubscribeConsoleNotifier("Where: ");

Выдаёт это. Обратите внимание, что LogToConsole показывает вывод от Range перед тем, как срабатывает фильтр Where:

Range:   OnNext(0)
Where:   OnNext(0)
Range:   OnNext(1)
Range:   OnNext(2)
Where:   OnNext(2)
Range:   OnNext(3)
Range:   OnCompleted()
Where:   OnCompleted()

Очевидно, вы можете использовать этот подход, чтобы написать логгер, или что-то более полезное.

Чтобы завершить эту запись, вот реализация метода NotifyToString:

public static string NotifyToString<T>(
    Notification<T> notification, string key = null)
{
    if (key != null)
        return key + "\t" + notification;
    
    return notification.ToString();
}

Это всё довольно тривиально, но весьма полезно, по-моему.

Файл CHM не показывается: Причины и решения

перевод

Итак, вы в затруднении. Некоторые или даже все ваши CHM файлы, похоже, поломались. Вместо содержимого вы видите «The page cannot be displayed». Существует несколько возможных причин, почему ваши CHM файлы стали нечитаемы.

1. Компонент «Просмотровщик CHM» неправильно зарегистрирован в системе

Системный файл <WINDOWS>\System32\hhctlr.ocx пропал, испорчен или разрегистрирован.

Решение: Выполнить команду «regsvr32 hhctrl.ocx» из командной строки чтобы зарегистрировать компонент в системе.

2. Ваши CHM файлы находятся в папке с символом ‘#’ (решётка)

Множество C# разработчиков обнаружили, что их документация и электронные книги в CHM формате не могут быть прочитаны потому что лежат в каталоге вроде ‘C:\E-books\C#\’

Символ «решётка» используется в HMTL для обозначения ссылок, поэтому просмотровщик CHM не может правильно определить путь к файлу и достать содержимое.

Решение: Удалите символ ‘#’ (решётка) из имени каталога. Также избегайте символов ‘?’, ‘&’ и ‘+’ в именах каталогов.

3. Обновление безопасности для Windows XP может заблокировать доступ к CHM файлам

Обновление безопасности для Windows XP блокирует активное содержимое в CHM файлах чтобы защитить вашу систему.

Решение: Запустите Проводник, щёлкните правой кнопкой мыши на CHM файле, выберите Свойства (Properties) из всплывающего меню. Нажмите кнопку «Разблокировать» (Unblock). Нажмите кнопку «Применить» (Apply). Когда CHM файл разблокирован кнопка «Разблокировать» исчезнет.

4. CHM файл находится в ограниченной зоне узлов Internet

Когда CHM файл, к которому вы хотите получить доступ, сохранён удалённо, то есть на сетевом диске или удалённом сервере, вы также можете обнаружить проблемы с доступом. Если ассоциированная зона защиты ограниченная, файл CHM не будет отображён правильно.

Решение: Модифицировать элемент реестра ItssRestrictions чтобы разрешить особенную зону защиты. Следуйте этим шагам:

  1. Запустите ‘regedit’ в командной строке.
  2. Выберите и щёлкните на следующем ключе:
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\HTMLHelp\1.x\ItssRestrictions
    Примечание: Если этот ключ не существует, создайте его.
  3. Щёлкните правой кнопкой мыши на подключе ItssRestrictions, выберите Создать, и, затем, Параметр DWORD (32 бита).
  4. Наберите MaxAllowedZone и нажмите ENTER.
  5. Нажмите правой кнопкой мыши на MaxAllowedZone и выберите Изменить.
  6. Введите значение от 0 до 4 и нажмите ОК.
    Значения этого параметра соответствуют:
    0 = Мой компьютер
    1 = Местная интрасеть
    2 = Надёжные сайты
    3 = Интернет
    4 = Опасные сайты
    Для большинства CHM файлов значения 1 должно быть достаточно чтобы получить доступ
  7. Закройте редактор реестра.

Предупреждение: Разрешайте только те зоны, которым вы доверяете. Не разрешайте зоны, в который вы не уверены.

Перевод строки в регулярных выражениях

Недавно, я встретил такой вопрос:

string test = "str1\nstr2";
Regex regex1 = new Regex("str1\nstr2");
Regex regex2 = new Regex("str1\\nstr2");
Regex regex3 = new Regex("str1\\\nstr2");
Console.WriteLine(regex1.IsMatch(test));
Console.WriteLine(regex2.IsMatch(test));
Console.WriteLine(regex3.IsMatch(test));

Почему все три регулярных выражения возвращают true?

Вопрос показался мне интересным, и я потратил время на его решение.

В первом случае у вас шаблон регулярного выражения содержит код последовательность символов «\n» (перевод строки) которая при компиляции превращается в символ перевода строки. То же самое происходит с тестируемой строкой. То есть шаблон регулярного выражения и тестируемая строка полностью совпадают.

Во втором случае шаблон содержит последовательность «\\n», которую компилятор превращает в строку из двух символов: обратный слэш и малая латинская n — «\n». Движок регулярных выражений .NET распознаёт этот шаблон и сопоставляет его символу «перевод строки» из тестируемой строки. В результате — шаблон совпадает.

В третьем случае шаблон содержит последовательность «\\\n», которую компилятор превращает в строку из двух символов: <обратный слэш> и <перевод строки>. Движок при разборе встречает <обратный слэш>, и проверяет следующий за ним символ. У движка есть правила для \s \S \w \W и т.д., но для <обратный слэш> <перевод-строки> правила нет, поэтому он игнорирует <обратный слэш> и продолжает работу как будто его не было. Забавный момент — получается чтобы сопоставить шаблон с обратным слэшом нам нужно в коде написать его 4 раза подряд — первое удвоение «съест» компилятор, второе — для движка регулярных выражений.

Совет: чтобы упростить себе работу с регулярными выражениями используйте verbatim string. Это строковый литерал, слева от открывающей кавычки которого стоит символ @. Для таких строк компилятор не производит преобразование escape-последовательностей с обратным слэшом.

Ссылки:

Советы и трюки WPF – Вывод всех дат, времён, чисел… в текущих региональных настройках

перевод

Это первая запись из серии, в которой я постараюсь публиковать по крайней мере один трюк в день, и который вы сможете использовать в своей разработке под .NET. Некоторые из них я придумал сам, другие подсмотрел на форумах. Я буду давать ссылки там где они необходимы 🙂

У WPF есть очень раздражающая особенность. Все даты выводятся по умолчанию в формате en-US. Сначала я объясню, почему так происходит, а затем дам несколько возможных решений и одно — моё любимое.

Как вы знаете, класс UIElement имеет свойство Language. Предполагается, что это свойство определяет язык, на котором элемент выводит своё содержимое. Таким образом WPF сможет узнать что какое-то содержимое на американском английском (en-US), а какое-то на британском (en-GB), и т.д. Оно также привязано к свойству xml:lang, которое можно установить у любого XML документа.

Если вы прочитаете документацию, как когда-то сделал я, вы узнаете, что xml:lang по умолчанию установлен в пустую строку, и, таким образом, не имеет ассоциированной культуры. Однако, по умолчанию соответствующее свойство Language имеет значение… en-US! А из-за того, что работают привязки, все ваши даты всегда показываются в американском формате.

Когда бы вы не привязали свойство к объекту типа DateTime, привязка преобразует тип DateTime в тип, которые ожидает свойство. Возьмём, к примеру, такую привязку:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <Grid>
        <TextBlock Text="{Binding Path={x:Static sys:DateTime.Now}}" />
    </Grid>
</Window>

Тут произойдёт преобразование DateTime в String — тип свойства Text. Но чтобы это сделать используется конвертер по умолчанию. Если вы проверите документацию на привязки, вы увидите, что можно указать IValueConverter в свойстве Converter. Также там упомянуто свойство ConverterCulture, которое специфицирует, какую культуры вы хотите использовать для преобразования своих данных — в нашем случае экземпляра DateTime.

Как я уже говорил, без специфицированного Converter WPF автоматически найдёт его для вас: либо встроенный, либо используя существующую инфраструктуру преобразования. Но угадайте, что произойдёт, если вы не укажете ConverterCulture?

WPF выберет культуру из свойства Language того элемента, с которым происходит привязка. В нашем случае это снова вариант по умолчанию: en-US!

Итак, вот мой трюк. Чтобы установить культуру по умолчанию вашего приложения равной текущей культуре на машине клиента во время выполнения, вам нужно добавить один простой кусочек кода в файл App.xaml.cs.

public partial class App : Application
{
    static App()
    {
        FrameworkElement.LanguageProperty.OverrideMetadata(
            typeof(FrameworkElement),
            new FrameworkPropertyMetadata(
                XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
    }
}

Этот кусочек кода переопределяет значение по умолчанию свойства Language у всех наследников FrameworkElement в вашем приложении на текущую культуру приложения. В этом случае мы заинтересованы в переопределении этого значения, чтобы любое содержимое по умолчанию предполагалось на языке, на который вы хотите локализовать приложение. Это немного неправильно, как объяснил Майкл в своем посте Зачем нам и CurrentCulture и CurrentUICulture, но это решение, приближается к наиболее приемлемому результату.

Что же Microsoft вероятно следует сделать? Установить значение по умолчанию для свойства Binding.ConverterCulture в значение CultureInfo.CurrentCulture. Но это только моё мнение 🙂