« Все записи

C#/.NET: пять маленьких чудес, которые делают код лучше (часть 1 из 3)

Джеймс Майкл Харе (James Michael Hare) написал отличную серию статей "Маленькие чудеса C#". На мой взгляд, ее должен прочесть каждый разработчик, я, хоть далеко и не новичок, нашел в ней для себя немало интересного. Я не нашел этого цикла на русском, отдельные отрывки попадались, поэтому решил поробовать перевести всё. Сегодня - превая часть из оригинальной трилогии.

Update: вторая часть трилогии.
Update: третья часть трилогии.

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

1. Оператор ??

Это такой элегантный маленький оператор, который может быть чрезвычайно полезным в некоторых ситуациях. Ведь часто у вас есть переменная, значение которой вы хотите использовать, но если это значение равно null, то вы хотите заменить его значением по умолчанию? Скажем, вы хотите присвоить строку переменной, но если это строка null,  вы хотите подставить пустую строку вместо нее?

Можно, конечно, написать условное выражение с if:

string name = value;
 
if (value == null)
{
    name = string.Empty;
}

Или еще лучше, если вы используете тренарный оператор (?:):

string name = (value != null) ? value : string.Empty;

О, это уже гораздо более кратко, но мы можем получить код еще лучше! В C# имеется оператор ??, представляющий собой укороченную версию тренарного оператора (?:) с проверкой на значение null:

string name = value ?? string.Empty;

Очень красиво и лаконично! Вы можете даже написать вспомогательный метод, чтобы урезать строку и возвращать нулевое значение, если строка состоит только из пробелов, так что он может использовать "??".

public static class StringUtility
{
    public static string TrimToNull(string source)
    {
        return string.IsNullOrWhiteSpace(source) ? null : source.Trim();
    }
}

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

string name = StringUtility.TrimToNull(value) ?? "Не определено";

2. Приведение As

Сколько раз вы видели такой код:

if (employee is SalariedEmployee)
{
    var salEmp = (SalariedEmployee)employee;
 
    pay = salEmp.WeeklySalary;
 
    // ...
}

Он является излишним, потому что вы выполняете проверку типа дважды. Один раз - это проверка, и еще один - это приведение типа. Всякий раз когда вы обнаружили, что идете по этому пути, предпочтите приведение as. Этот удобное приведение, поскольку оно возвращает тип, если типы совместимы, или возвращает значение null, если нет:

var salEmployee = employee as SalariedEmployee;
 
if (salEmployee != null)
{
    pay = salEmployee.WeeklySalary;
 
    // ...
}

Код читается лучше без уродливых приведений, а вы избегаете двойной проверки типа.

3. Авто-свойства

Мы все знаем о свойствах C#, и большинство из нас знает об автоматических свойствах. Они превращают такой код с двумя типичными свойствами с внутренними полями:

public class Point
{
    private int _x, _y;

    public int X 
    {
        get { return _x; }
        set { _x = value; }
    }

    public int Y
    {
        get { return _y; }
        set { _y = value; }
    }
}

в гораздо более краткий и сопровождаемый кусок кода:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Намного короче! Всякий раз, когда у вас есть простое свойство, котороу просто возвращает и устанавливает значение внутреннего поля, вам следует отдать предпочтение авто-свойствам, хотя бы для того, чтобы много не печатать! Все что вам нужно, это написать свойства объекта без кода внутри методов get/set и компилятор C# автоматически сгенерирует поля для вас "за кулисами".

Чтобы получить еще больше удовольствия, вы можете сделать авто-свойства с асимметричными уровнями доступа! То есть вы можете сделать авто-свойство "только для чтения" или "только для записи". То есть вы можете сделать get публичным, а set - частным или  наоборот.

public class Asymetrical
{
    public string ThisIsReadOnly { get; private set; }
 
    public double ThisIsWriteOnly { private get; set; }
}

Это делает легой задачей создание авто-свойств, которые могут быть установлены только самим классом, но прочитны публично и наоборот (хотя, на практике свойства "только для записи" применяются редко).

4. Класс Stopwatch (секундомер)

Сколько раз вы хотели засечь, сколько времени выполняется определенный кусок кода? Может быть вы пытались отслеживать среднее время выполнения запроса или другие подобные метрики, или, возможно, хотели отправлять предупреждающее сообщение, когда хранимая процедура треюует более чем 1 секунду времени для выполения.

Ну вы могли бы сделать это с DateTime вот так:

DateTime start = DateTime.Now;
SomeCodeToTime();
DateTime end = DateTime.Now;
Console.WriteLine("Выполнение метода заняло {0} мс", (end - start).TotalMilliseconds);

Но DateTime неточен для такого рода измерений. Вы также можете обратиться к таймеру высокого разрешения с помощью вызовы Win32 API через PInvoke, но этот способ тоже очень грязный и более подвержен ошибкам.

Так что же разработчику C# делать? Используйте класс Stopwatch (cекундомер), который находится в пространстве имен System.Diagnostics. Класс Stopwatch делает чистым и простым в использовании таймер высокого разрешения через удобства класса C#:

var timer = Stopwatch.StartNew();
SomeCodeToTime();
timer.Stop();
Console.WriteLine("Выполнение метода заняло {0} мс", timer.ElapsedMilliseconds);

Гораздо более точным и очень чистым. Обратите внимание, что есть статический фабричный метод, который создает секундомер и сразу же запусткает его. У вас также есть возможность создать секундомер с использованием ключевого слова new, и затем запустить его вручную, когда понадобится.

5. Фабричные методы TimeSpan

Сколько раз вы видели такой код, как этот, и интересовались, как долго должен длиться сон?

Thread.Sleep(50);

50 секунд? Миллисекунд? Это миллисекунды на самом деле, но что делать, если наткнулся на что-то подобное в коде кто-то:

void PerformRemoteWork(int timeout) { ... }

Что это за тайм-аут? Секунды? Минуты? Миллисекунды? Все зависит от разработчиков! Я видел, тайм-ауты и интервалы в BCL и пользовательском коде, записанные и как секунды и как миллисекунды. Для этого почти всегда лучше использовать TimeSpan, поскольку он делает это гораздо менее двусмысленным:

void PerformRemoteWork(TimeSpan timeout) { ... }

Теперь нам не нужно беспокоиться о единицах, потому что они были указаны при создании TimeSpan:

PerformRemoteWork(new TimeSpan(0, 0, 0, 0, 50));

Устраняется неоднозначность с точки зрения самого метода, но не вызывающего! Для вызывающего пройдут 50 секунд? Или миллисекунд? Кажется, у нас уже есть подобная проблема! Некоторую путаницу вности еще и то, что есть 5 конструкторов для TimeSpan и не все они однозначны:

TimeSpan();
TimeSpan(long ticks);
TimeSpan(int hours, int minutes, int seconds);
TimeSpan(int days, int hours, int minutes, int seconds);
TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds);

Обратите внимание, что 3-й конструктор содержит часы, минуты и секунды, а 4-й конструктор добавляет дни? Если вы хотите задать миллисекунды, нужен 5-й конструктор. Это может вызвать путаницу, поскольку 4-го конструктор может прочитать как часы/секунды/минуты/миллисекунды, но это не так.

Введите фабричные статические методы TimeSpan. Это позволит вам конструировать TimeSpan однозначно:

PerformRemoteWork(TimeSpan.FromMilliseconds(50));

Теперь нет никакой двусмысленности, и этот прекрасно прекрасно читается! Нет шансов неправильно истолковывать параметры. По аналогии:

TimeSpan.FromDays(double days);
TimeSpan.FromHours(double hours);
TimeSpan.FromMinutes(double minutes);
TimeSpan.FromSeconds(double seconds);

Так что вы можете просто указывать интервал однозначным образом. Это даже работает со статическими полями только для чтения:

public sealed class MessageProducer
{
        private static readonly _defaultTimeout = TimeSpan.FromSeconds(30);
        . . .
}

Резюме

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

Продолжение последует...

Update: вторая часть трилогии.
Update: третья часть трилогии.

Progg it

comments powered by Disqus