Методы расширения являются фундаментальной частью C# и объектно-ориентированного программирования (ООП). Методы расширения в C# позволяют “расширять” существующие типы, включая классы, интерфейсы или структуры, без изменения их исходного кода.

Это особенно полезно, когда вы хотите добавить новую функциональность к типу, который вы не владеете или не можете изменить, например, типы из библиотек сторонних разработчиков или встроенные типы .NET, такие как string, List<T> и так далее.

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

Содержание

Как создать методы расширения для DateTime

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

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

Методы расширения должны находиться в статическом классе, потому что:

  1. Объект не требуется: Вам не нужно создавать объект для использования метода расширения. Поскольку метод добавляет новую функциональность к существующему типу (например, string), он может работать без необходимости в экземпляре класса.

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

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

Сначала давайте создадим статический класс DateTimeExtensions.

public static class DateTimeExtensions {

}

Это будет охватывать все расширения DateTime, которые мы хотим создать.

public static bool IsWeekend(this DateTime date)
{
    return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}

Объяснение:

public static bool IsWeekend: Это определяет, что это статический метод с именем IsWeekend, который будет возвращать значение bool (истина/ложь).

this DateTime date: Ключевое слово this в аргументе метода указывает на то, что этот метод является методом расширения. Это означает, что метод будет расширением класса DateTime.

Как связывать методы расширения одного типа

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

using System.Globalization;

public static string ToTitleCase(this string str)
{
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower());
}

public static string TrimAndAppend(this string str, string toAppend)
{
    return str.Trim() + toAppend;
}

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

Обратите внимание, что мы предоставили только второй параметр методу TrimAndAppend, так как первый параметр — это строка, к которой применяется метод расширения (как объяснялось ранее, обозначено ключевым словом this).

var title = "hello world   "
    .ToTitleCase()
    .TrimAndAppend("!!");

//Вывод:
// Привет, мир!!

Если метод расширения возвращает другой тип (не оригинальный или совместимый), вы не можете его связывать. Например:

var date = new DateTime();
date.IsWeekend().AddDays(1);

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

Здесь у нас есть метод с именем IsWeekend(), который возвращает логическое значение. Затем мы пытались вызвать AddDays(1) для логического значения, которого не существует, так как это расширение для DateTime. Компилятор кода не сможет построить код и выдаст ошибку, информируя вас об этом.

Как вернуть экземпляр для цепочки

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

Давайте рассмотрим пример списка автомобилей.

public static List<T> RemoveDuplicates<T>(this List<T> list)
{
    // Используйте Distinct, чтобы удалить дубликаты и обновить список
    list = list.Distinct().ToList();

    // Верните измененный список для цепочки методов
    return list;
}

public static List<T> AddRangeOfItems<T>(this List<T> list, IEnumerable<T> items)
{
    // Добавьте диапазон элементов в список
    list.AddRange(items);

    // Верните измененный список для цепочки методов
    return list;  
}

Теперь, когда мы вернули список из этих методов расширения, мы можем цеплять дополнительные методы к тому же списку. Например, после удаления дубликатов с помощью RemoveDuplicates(), мы можем сразу вызвать AddRangeOfItems() для того же списка.

Так что мы можем сделать что-то вроде:

var existingStock = new List<string> { "Ford", "Jaguar", "Ferrari", "Ford", "Renault" };

var availableBrands = existingStock
    .RemoveDuplicates()
    .AddRangeOfItems(new[] { "Lamborghini" }); // новый запас доступен

Console.WriteLine("Brands Available Now: " + string.Join(", ", availableBrands));

// Вывод: Доступные бренды сейчас: Ford, Jaguar, Ferrari, Renault, Lamborghini

Мы удалили дубликаты из списка марок автомобилей и добавили новый товар в тот же список. Это работает, потому что RemoveDuplicates возвращает список, что позволяет нам связывать его с AddRangeOfItems.

Если бы RemoveDuplicates возвращал void вместо списка, мы не смогли бы связывать методы. Он все равно удалял бы дубликаты, но дальнейшие действия, такие как добавление нового товара, были бы невозможны в том же выражении.

Нам также пришлось бы обновить RemoveDuplicates, чтобы обновить переданный аргумент списка, так как Distinct() возвращает новый список, который не возвращается, как показано ниже, что, я думаю, вы согласитесь, гораздо более многословно.

public static void RemoveDuplicates<T>(this List<T> list)
{
    // Получить уникальные элементы и очистить оригинальный список
    var distinctItems = list.Distinct().ToList();
    list.Clear(); 

    // Добавить уникальные элементы обратно в оригинальный список
    list.AddRange(distinctItems);
}

Почему я не могу просто добавить эти методы в свой класс?

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

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

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

Предположим, вы используете класс FileInfo из пространства имен System.IO для работы с файлами. Вы можете захотеть добавить метод для простого проверки, является ли файл слишком большим (например, более 1 ГБ), но вы не можете изменить класс FileInfo напрямую, потому что он принадлежит пространству имен System.IO (то есть он встроен в .Net).

Без расширения:

var fileInfo = new FileInfo("myFile.txt");

if (fileInfo.Length > 1024 * 1024 * 1024) // размер файла больше 1 ГБ
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

С методом расширения:

Вы можете сделать это более универсальным, добавив метод расширения, который проверяет, превышает ли файл 1 ГБ.

public static class FileInfoExtensions
{
    // метод расширения с размером файла по умолчанию 1 ГБ (можно переопределить)
    public static bool IsFileTooLarge(this FileInfo fileInfo, long sizeInBytes = 1024 * 1024 * 1024)
    {
        return fileInfo.Length > sizeInBytes;
    }
}

Теперь вы можете использовать метод IsFileTooLarge непосредственно на объектах FileInfo, что делает ваш код более чистым:

csharpCopy codevar fileInfo = new FileInfo("myFile.txt");

if (fileInfo.IsFileTooLarge())
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

Расширение сторонних библиотек и пакетов может сделать ваш код гораздо более совместимым.

Лучшая организация и читаемость: Вы можете организовать методы расширения в статические классы на основе функциональности или контекста, что упрощает их поиск и использование. Это, безусловно, улучшено благодаря возможности цепочечного вызова методов расширения.

Когда использовать расширения

  • Для утилитарных методов: Если у вас есть утилитарные методы, которые полезны для типа, но не принадлежат непосредственно самому типу (например, форматирование, валидация).

  • Для улучшения встроенных типов: Если вы хотите добавить функциональность к встроенным типам (например, string или DateTime) без их изменения.

  • Когда вы хотите, чтобы методы были опциональными: Если вы хотите предоставить дополнительные методы, которые пользователи могут выбрать для использования, не заставляя их интегрировать их в основное проектирование класса.

Пример сценария

Представьте, что у вас есть класс Person, и вы хотите добавить метод для красивого форматирования имени человека:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// Метод расширения в статическом классе
public static class PersonExtensions
{
    public static string GetFullName(this Person person)
    {
        return $"{person.FirstName} {person.LastName}";
    }
}

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

Когда не использовать методы расширения

  • Для основной функциональности: Если метод является неотъемлемым для основного поведения класса, он должен быть частью самого класса, а не расширением.

  • Для плотной связанности: Если метод расширения требует глубокого понимания приватного состояния класса или нуждается в регулярном доступе к его внутренней логике.

  • Для публичных API: При разработке библиотеки или API, предназначенного для публичного использования, часто лучше включать необходимые методы непосредственно в класс, а не заставлять пользователей искать или создавать свои методы расширения.

Вещи, которые следует учитывать при проектировании расширений

Хотя методы расширения являются мощными и удобными во многих случаях, есть определенные недостатки или ситуации, когда их использование может не быть лучшим выбором:

Скрытое поведение/путаница

  • Методы расширения не отображаются непосредственно в определении класса, что означает, что их может быть труднее обнаружить разработчикам, незнакомым с доступными расширениями.

  • Разработчикам необходимо знать, что существуют эти методы расширения, иначе они могут не использовать их, если не работают в IDE с такими функциями, как IntelliSense (например, Visual Studio, JetBrains Rider). Эти IDE могут предлагать методы расширения из других файлов или пространств имен, когда они обнаруживают соответствующий тип. Без функционально насыщенной IDE разработчику нужно будет знать о методах расширения или найти папку, в которой они хранятся.

Невозможно получить доступ к закрытым членам

  • Методы расширения могут получать доступ только к членам (методам, свойствам, полям), которые являются публичными или внутренними.

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

Пример:

public class Car
{
    private string engineNumber = "12345"; // Приватное поле

    public string Brand { get; set; } = "Ford"; // Публичное свойство

    private void StartEngine() // Приватный метод
    {
        Console.WriteLine("Engine started");
    }
}
public static class CarExtensions
{
    public static void DisplayBrand(this Car car)
    {
        Console.WriteLine($"Brand: {car.Brand}"); // Доступ к публичному свойству 'Бренд'
    }

    public static void TryAccessPrivateField(this Car car)
    {
        // Невозможно получить доступ к приватному 'номер двигателя'
        // Это приведет к ошибке компиляции.
        Console.WriteLine(car.engineNumber);
    }
}

Дублирование кода и чрезмерное использование

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

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

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

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

Заключение

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

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