« Все записи

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

Вторая часть марлезонского балета трилогии "Маленькие чудеса C#" Джеймса Майкла Харе.

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

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

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

1. string.IsNullOrEmpty() и string.IsNullOrWhiteSpace()

Удивительно, как много людей не знают об этих двух статических вспомогательных методах, которые изящно дополняют класс string. Статический метод string.IsNullOrEmpty() был доступен в Framework 2.0, но Framework 4.0 дал нам еще один "драгоценный камень": string.IsNullOrWhitespace().

Функции этих методов должны быть понятны из их названия: первый проверяет, является ли строка нулевой (null) или пустой (Length == 0), а второй проверяет, является ли строка нулевой или состоит из одних пробельных символов.

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

public string GetFileName(string fullPathFileName)
{
    // we can either check Length to see if empty or compare to string.Empty
    if (fullPathFileName == null || fullPathFileName.Length == 0)
    {
        // bad, must have a path specified!
        throw new ArgumentNullException(fullPathFileName);
    } 
 
    ...
}

Это не ужасно, но вот это выглядит гораздо точнее, так сказать:

public string GetFileName(string fullPathFileName)
{
    // first way to do this is to check for null and a positive length
    if (string.IsNullOrEmpty(fullPathFileName))
    {
        // bad, must have a path specified!
        throw new ArgumentNullException(fullPathFileName);
    } 
 
    ...
}

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

А что c проверкой на null или пробелы? Скажем, вы соединяете имя, отчество и фамилию, и не хотите иметь двойной пробел, если отчество окажется пустым:

public string GetFullName(string firstName, string middleName, string lastName)
{
    if (middleName == null || middleName.Trim().Length == 0)
    {
        return string.Format("{0} {1}", firstName, lastName);
    } 
 
    return string.Format("{0} {1} {2}", firstName, middleName, lastName);
}

Обратите внимание, мы вызвали Trim() для удаления пробелов, а затем проверили свойство Length (длина). Хотя этот код и кажется хорошим и кратким, он создает новый объект string в куче, который должен быть позже удален сборщиком мусора. В принципе, такие строки здесь или там не убьют производительность, но в программе с высокими требованиями к производительности вы захотите сохранять минимум мусора, особенно если есть и другие, как опции сопровождения!

Просто используйте новую функцию .NET 4.0 string.IsNullOrWhitespace(). Это новый метод проверки, является ли строка null или содержит только пробелы:

public string GetFullName(string firstName, string middleName, string lastName)
{
    if (string.IsNullOrWhiteSpace(middleName))
    {
        return string.Format("{0} {1}", firstName, lastName);
    } 
 
    return string.Format("{0} {1} {2}", firstName, middleName, lastName);
}

Это более краткий код, мы не должны беспокоиться о правильных логических операторах, и он не создает каких-либо дополнительных строковых объектов, которые должны удаляться сборщиком мусора!

2. string.Equals()

Набор методов string.Equals() намного более разнообразен, чем вы, видимо, ожидаете. Есть много вариантов использования этих методов, о некоторых из которых, к сожалению, забывают.

Прежде всего, знаете ли вы, что существует статический string.Equals() метод? Зачем нам это знать? Ну а что, если есть возможность, что проверяемая строка (которая сравнивается) окажется нулевой (null)? Давайте посмотрим:

public Order CreateOrder(string orderType, string product, int quantity, double price)
{
    if (orderType.Equals("equity"))
    {
        // ...
    } 
 
    // ...
}

Что произойдет, если orderType окажется равным null? Очевидно, что это вызовет NullReferenceException. Конечно, мы могли бы проверить это перед тем как вызвать наш метод, но теперь, когда вы делаете сравнение строк, над котрыми у вас мало контроля, если у вас есть обоснованные сомнения, что одна из строк может быть null, вместо кода:

if (orderType != null && orderType.Equals("equity"))

вы можете использовать статический string.Equals(), который можно с уверенностью вызывать, даже если один или оба аргумента являются null:

if (string.Equals(orderType, "equity"))

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

if (orderType.ToUpper().Equals("EQUITY"))

Это работает, но это функционально неверно, в очередной раз вы создаете новую строку (возвращается из ToUpper()), которая затем должна быть удалена сборщиком мусора! Если это действительно высокопроизводительный процессор, то дополнительный мусор не уничтожит вас, но и не сделает быстрее тоже! Часто упускаются из виду перегрузки метода string.Equals() (экземпляра и статическая), которые позволяют вам выполнять сравнение, не чувствительное к регистру:

if (orderType.Equals("equity", StringComparison.InvariantCultureIgnoreCase))

или, если вы предполагаете, что строка может быть нулевой (null), то вы можете сделать то же самое, используя статический метод string.Equals():

if (string.Equals(orderType, "equity", StringComparison.InvariantCultureIgnoreCase))

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

3. Оператор using

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

public IEnumerable<Order> GetOrders()
{
    var orders = new List<Order>(); 
 
    var con = new SqlConnection("some connection string");
    var cmd = new SqlCommand("select * from orders", con);
    var rs = cmd.ExecuteReader(); 
 
    while (rs.Read())
    {
        // ...
    } 
 
    rs.Dispose();
    cmd.Dispose();
    con.Dispose(); 
 
    return orders;
}

Ух-ты, во-первых, это по-своему уродливо, и во-вторых, если у вас возникнет исключение где-то между созданием экземпляра SqlConnection и последним Dispose(), вы рискуете не только не вызывать Dispose() для других ресурсов, но это может привести к тому, что останутся соединения с БД, которые не будут корректно освобождены немедленно. Да, они В КОНЕЧНОМ ИТОГЕ будут убраны сборщиком мусора, но до тех пор вы будете держать ценные внешние ресурсы открытыми!

Ну, в принципе, мы могли бы защититься от этого с помощью блока finally:

public IEnumerable<Order> GetOrders()
{
    SqlConnection con = null;
    SqlCommand cmd = null;
    SqlDataReader rs = null; 
 
    var orders = new List<Order>(); 
 
    try
    {
        con = new SqlConnection("some connection string");
        cmd = new SqlCommand("select * from orders", con);
        rs = cmd.ExecuteReader(); 
 
        while (rs.Read())
        {
            // ...
        }
    } 
 
    finally
    {
        rs.Dispose();
        cmd.Dispose();
        con.Dispose();
    } 
 
    return orders;
}

Но даже это имеет проблемы! Что если SqlCommand потерпит неудачу и вызовет исключение, ведь reader может оказаться пустым (null), и в этом случае rs.Dispose() тоже вызовет исключение, а connection никогда не получит Disposed(). Конечно, мы могли бы выполнить проверку на null с помощью if перед каждым Dispose(), но, черт побери, использование оператора using делает это все это таким простым:

public IEnumerable<Order> GetOrders()
{
    var orders = new List<Order>(); 
 
    using (var con = new SqlConnection("some connection string"))
    {
        using (var cmd = new SqlCommand("select * from orders", con))
        {
            using (var rs = cmd.ExecuteReader())
            {
                while (rs.Read())
                {
                    // ...
                }
            }
        }
    } 
 
    return orders;
}

О, ведь так гораздо проще! Оператор using вызовет Dispose() для экземпляра немедленно, когда тот выйдет из его области действия из-за достижения конца блока или из-за исключения, которое заставит экземпляр покинуть блок преждевременно! Заметьте, мы не должны делать грязные проверки на null, и мы не должны иметь большой уродливый блок try/finally, и не должны предварительно инициализировать наши переменные в помощью null, чтобы они были видны в блоке finally. Так намного чище!

Что вы говорите? Вам не нравится эти сложные отступы? Ну, имейте в виду, что оператор using может быть использован в простой или сложной форме. То есть, если вы не ставите фигурные скобки после оператора using, то он свою область действия инкапсулирует на следующий оператор. Можно расположить их так:

public IEnumerable<Order> GetOrders()
{
    var orders = new List<Order>(); 
 
    using (var con = new SqlConnection("some connection string"))
    using (var cmd = new SqlCommand("select * from orders", con))
    using (var rs = cmd.ExecuteReader())
    {
        while (rs.Read())
        {
            // ...
        }
    } 
 
    return orders;
}

Первый using объявляет переменную con (да, вы можете использовать var в using для краткости) и область ее действия (и, соответственно, освобождения) после следущего оператора, который является другим блоком using и так далее! Здесь нет сложных отступов, все выглядит красиво, четко и кратко!

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

using (Font font3 = new Font("Arial", 10.0f),
            font4 = new Font("Arial", 10.0f))
{
    // Use font3 and font4.
}

4. Модификатор класса static

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

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

public class XmlUtility
{
    public string ToXml(object input)
    {
        var xs = new XmlSerializer(input.GetType()); 
 
        using (var memoryStream = new MemoryStream())
        using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding()))
        {
            xs.Serialize(xmlTextWriter, input);
            return Encoding.UTF8.GetString(memoryStream.ToArray());
        }        
    }
}

Это типичный код XML-сериализации. Проблема в том, что мы должны создать этот класс, чтобы его можно было использовать:

var xmlUtil = new XmlUtility();
string result = xmlUtil.ToXml(someObject);

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

public class XmlUtility
{
    // create private constructor so this class cannot be created or inherited
    private XmlUtility()
    {
    }
    public static string ToXml(object input)
    {
        var xs = new XmlSerializer(input.GetType()); 
 
        using (var memoryStream = new MemoryStream())
        using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding()))
        {
            xs.Serialize(xmlTextWriter, input);
            return Encoding.UTF8.GetString(memoryStream.ToArray());
        }        
    }
}

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

public T FromXml<T>(string xml) { ... }

Так как он не был объявлен статическим, но конструктора не видно, этот метод никогда не может быть использован. Введите модификатор класса static. Если вы поместите слово static перед ключевым словом class, оно сообщит компилятору, что класс должен содержать только статические методы, и его экземпояр не может быть создан, а класс не может быть наследован.

public static class XmlUtility
{
    public static string ToXml(object input)
    {
        var xs = new XmlSerializer(input.GetType()); 
 
        using (var memoryStream = new MemoryStream())
        using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding()))
        {
            xs.Serialize(xmlTextWriter, input);
            return Encoding.UTF8.GetString(memoryStream.ToArray());
        }        
    }
}

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

Помните, что всякий раз переводя потенциальные логические ошибки в потенциальные ошибки компилятора, вы получаете гораздо больше работоспособности обратно!

5. Инициализаторы объектов и коллекций

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

Последняя приманка про производительность может немного удивить вас, так как в основном синтаксис инициализации - это "синтаксический сахар". Для тех из вас кто не знает, синтаксис инициализации позволяет задать значения для доступных полей и свойств во время построения. Давайте сначала посмотрим на инициализаторы объекта с простого примера.

Давайте представим типичную структуру Point:

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

а теперь мы создадим и инициализируем экземпляр:

var startingPoint = new Point();
startingPoint.X = 5;
startingPoint.Y = 13;

Выглядит как типичный код создания и заполнения, не так ли? Ну а с инициализатом объектов мы можем написать все в одной строке, вот так:

var startingPoint = new Point() { X = 5, Y = 13 };

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

Этот синтаксис доступен для создания любого типа, у которого имеются доступные поля или свойства и доступный конструктор.

Но теперь давайте посмотрим на инициализаторы коллекций. Предположим, мы хотим создать и загрузить список из 5 целых чисел:

var list = new List<int>();
list.Add(1);
list.Add(7);
list.Add(13);
list.Add(42);

Используя синтаксис инициализации коллекции, мы можем изменить код на:

var list = new List<int> { 1, 7, 13, 42 };

Еще раз гораздо более кратко! Опять обратите внимание, что сначала вызывается конструктор, а затем четыре раза вызывается метод Add() для каждого элемента в списке. Любопытно, что вам не нужно обязательно вызывать конструктор по умолчанию, например, если вы знаете, что список содержит 4 элемента, вы можете сначала опеределить вместимость списка во избежание по??енциального изменения размера списка при каждом вызове Add():

var list = new List<int>(4) { 1, 7, 13, 42 };

Сначала вызывается конструктор List<T>, который принимает целочисленный параметр, который называется capacity, а затем четыре раза вызывается Add(). Вы можете использовать это в своих коллекциях, все что нужно сделать, это реализовать интерфейс IEnumerable<T> и поддержку метода Add().

Вы также можете комбинировать инициализаторы объектов и коллекций, сравните следующий код:

var list = new List<Point>(); 
 
var point = new Point();
point.X = 5;
point.Y = 13;
list.Add(point);
point = new Point();
point.X = 42;
point.Y = 111;
list.Add(point);
point = new Point();
point.X = 7;
point.Y = 9;
list.Add(point);

с этим:

var list = new List<Point>
    {
        new Point { X = 5, Y = 13 },
        new Point { X = 42, Y = 111 },
        new Point { X = 7, Y = 9 }
    };

Какой для вас выглядит более чистым и более кратким? Лично мне нравится синтаксис инициализации. Даже тогда, когда код занимает несколько строк, то создается очень легко читаемый поток кода, который гораздо менее утомителен для глаз.

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

public class BeforeFieldInit
{
    public static List<int> ThisList = new List<int>() { 1, 2, 3, 4, 5 };
} 
 
public class NotBeforeFieldInit
{
    public static List<int> ThisList; 
 
    static NotBeforeFieldInit()
    {
        ThisList = new List<int>();
        ThisList.Add(1);
        ThisList.Add(2);
        ThisList.Add(3);
        ThisList.Add(4);
        ThisList.Add(5);
    }
}

Логически, это одно и то же: в обоих случаях создается статическое поле, которое будет содержать цифры от 1 до 5. Разница в том, что в первом из классов имеется явный статический конструктор, а во втором - нет. Для тех из вас, кто глубоко знает C#, вы знаете, что классы без явного статического конструктора могут быть отмечены флагом beforefieldinit для  инициализации полей при создании.

Давайте рассмотрим фрагмент IL для каждого случая:

.class public auto ansi beforefieldinit BeforeFieldInit
       extends [mscorlib]System.Object
{
} // end of class BeforeFieldInit 
 
.class public auto ansi NotBeforeFieldInit
       extends [mscorlib]System.Object
{
} // end of class NotBeforeFieldInit

Заметьте, что если класс имеет явный статический конструктор, то C# не отмечает класс флагом beforefieldinit в IL, и это означает, что перед любым доступом к статическому полю, он должен сделать быструю проверку, чтобы убедиться, что статический конструктор уже вызывался (для более подробной информации просто погуглите beforefieldinit). Это может нанести незначительный удар по производительности.

Теперь вы представьте: поскольку синтаксис инициализации является не просто вызовом конструктора, но также вызывает Add(), то он будет генерировать статический конструктор за кулисами для загрузки списка. И он это делает! Но поскольку это не явный статический конструктор, она все еще может быть отмечен флагом beforefieldinit и избежать дополнительной проверки.

Это также поможет очистить код вашего конструктора (статического или экземпляра) потому, что вы можете инициализировать коллекции в декларации (если известны значения) вместо того, чтобы загрузить ваши конструкторы большим количеством тривиальной логики.

Резюме

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

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

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

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

Progg it

comments powered by Disqus