Проектирование хороших расширений
Применяйте лучшие практики и избегайте типичных ошибок.
«Проектирование хороших расширений» — бесплатный урок C# Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Проектирование методов расширения, которые нравятся разработчикам
Методы расширения очень мощны, но их возможности легко использовать неправильно. Хорошие методы расширения ощущаются естественной частью типа, их легко находить, и они никогда не удивляют вызывающий код. В этом уроке рассматриваются практические рекомендации.
Расширяйте поведение, а не сущность типа
Предпочитайте методы расширения для вспомогательного поведения, которое хорошо читается в конвейере. Если метод действительно относится к принадлежащему вам типу, сделайте его настоящим членом этого типа.
using System;
public static class TimeExtensions
{
// A pleasant, focused helper
public static bool IsWeekend(this DayOfWeek day)
=> day == DayOfWeek.Saturday || day == DayOfWeek.Sunday;
}
public class Program
{
public static void Main()
{
Console.WriteLine(DayOfWeek.Sunday.IsWeekend());
Console.WriteLine(DayOfWeek.Monday.IsWeekend());
}
}По возможности сохраняйте методы чистыми
Методы расширения, которые возвращают новое значение и не имеют побочных эффектов, проще всего понимать и объединять в цепочки. Стремитесь к схеме: данные на входе, значение на выходе.
using System;
public static class StringExtensions
{
// Pure: depends only on input, returns a new string
public static string Truncate(this string s, int max)
=> s.Length <= max ? s : s.Substring(0, max) + "...";
}
public class Program
{
public static void Main()
{
Console.WriteLine("Extension methods are great".Truncate(10));
}
}Защищайтесь от нулевых входных значений
Поскольку вызывающий код может вызвать метод расширения для получателя null, заранее решите, что должно произойти. Либо корректно обрабатывайте значение null, либо выдавайте понятное исключение ArgumentNullException.
using System;
public static class StringExtensions
{
public static string SafeUpper(this string s)
{
if (s is null) throw new ArgumentNullException(nameof(s));
return s.ToUpper();
}
}
public class Program
{
public static void Main()
{
try { ((string)null).SafeUpper(); }
catch (ArgumentNullException ex) { Console.WriteLine("Caught: " + ex.ParamName); }
Console.WriteLine("ok".SafeUpper());
}
}Выбирайте имена для удобного поиска
Разработчики находят методы расширения, вводя точку и просматривая IntelliSense. Понятные имена в форме глагола, описывающие результат, делают вспомогательные методы заметными и самодокументируемыми.
using System;
using System.Collections.Generic;
public static class CollectionExtensions
{
public static bool HasItems<T>(this ICollection<T> c)
=> c != null && c.Count > 0;
}
public class Program
{
public static void Main()
{
var list = new List<int> { 1 };
Console.WriteLine(list.HasItems());
}
}Объединяйте связанные методы расширения
Помещайте методы расширения для одной концепции в один хорошо названный статический класс и одно пространство имён. Это сохраняет смысл импортов и позволяет пользователям подключать именно нужные им вспомогательные методы.
using System;
using System.Collections.Generic;
public static class EnumerableExtensions
{
public static IEnumerable<T> TakeFirst<T>(this IEnumerable<T> source, int n)
{
int taken = 0;
foreach (var item in source)
{
if (taken++ >= n) yield break;
yield return item;
}
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 1, 2, 3, 4 };
Console.WriteLine(string.Join(",", nums.TakeFirst(2)));
}
}Не скрывайте неожиданное поведение
Метод расширения, который выглядит дешёвым, но выполняет дорогую или зависящую от состояния работу, нарушает ожидания. Делайте стоимость очевидной: если метод обращается к базе данных или изменяет состояние, отразите это в его имени.
using System;
public static class MathExtensions
{
// Cheap and obvious: no hidden cost
public static int ClampMin(this int value, int min)
=> value < min ? min : value;
}
public class Program
{
public static void Main()
{
Console.WriteLine((-3).ClampMin(0));
Console.WriteLine(5.ClampMin(0));
}
}Избегайте расширения object
Расширение object засоряет каждый тип программы вашим методом и повсюду перегружает IntelliSense. Вместо этого выбирайте наиболее конкретный подходящий тип или интерфейс.
using System;
public static class IntExtensions
{
// Specific to int, not object - far less noisy
public static string ToOrdinal(this int n)
{
if (n % 10 == 1 && n % 100 != 11) return n + "st";
if (n % 10 == 2 && n % 100 != 12) return n + "nd";
if (n % 10 == 3 && n % 100 != 13) return n + "rd";
return n + "th";
}
}
public class Program
{
public static void Main()
{
Console.WriteLine(1.ToOrdinal());
Console.WriteLine(22.ToOrdinal());
}
}Для последовательностей предпочитайте возврат IEnumerable
Для операторов последовательностей возвращайте IEnumerable<T> и используйте отложенное выполнение. Это хорошо сочетается с другими операторами и не заставляет вызывающий код материализовывать данные, которые могут ему не понадобиться.
using System;
using System.Collections.Generic;
public static class SeqExtensions
{
public static IEnumerable<int> Doubled(this IEnumerable<int> source)
{
foreach (var n in source) yield return n * 2;
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 1, 2, 3 };
foreach (var n in nums.Doubled()) Console.WriteLine(n);
}
}Список проверок для хороших методов расширения
Перед публикацией метода расширения спросите себя: имя понятное? Метод чистый или побочные эффекты очевидны? Обрабатывается ли null? Достаточно ли конкретен целевой тип? Разумно ли сгруппирован метод? Если на всё можно ответить «да», вы создали вспомогательный метод, которым другим будет приятно пользоваться.
using System;
public static class StringExtensions
{
public static string OrDefault(this string s, string fallback)
=> string.IsNullOrEmpty(s) ? fallback : s;
}
public class Program
{
public static void Main()
{
Console.WriteLine("".OrDefault("N/A"));
Console.WriteLine("value".OrDefault("N/A"));
}
}Попробуйте сами
Примените список проверок: конкретный целевой тип, понятное имя, обработка null и чистый результат. Этот небольшой метод расширения соответствует всем рекомендациям.
using System;
using System.Collections.Generic;
public static class DictionaryExtensions
{
// Clear name, specific type, null-safe, pure
public static TValue GetOrDefault<TKey, TValue>(
this IReadOnlyDictionary<TKey, TValue> dict, TKey key, TValue fallback)
{
if (dict == null) return fallback;
return dict.TryGetValue(key, out var value) ? value : fallback;
}
}
public class Program
{
public static void Main()
{
var ages = new Dictionary<string, int> { ["Ann"] = 30 };
Console.WriteLine(ages.GetOrDefault("Ann", -1));
Console.WriteLine(ages.GetOrDefault("Bob", -1));
}
}Быстрая проверка
Примените рекомендации по проектированию.
Итоги
Хорошо спроектированные методы расширения выглядят естественно и предсказуемо.
- Предпочитайте чистые вспомогательные методы, возвращающие значения и хорошо объединяемые в цепочки.
- Определяйте и документируйте поведение при null.
- Используйте понятные имена в форме глагола, чтобы методы было легко находить.
- Ориентируйтесь на конкретные типы или интерфейсы, а не на
object. - Объединяйте связанные вспомогательные методы и возвращайте
IEnumerable<T>для последовательностей.
Изучай C# с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 93
- Уроки
- 346
Часто задаваемые вопросы
Урок «Проектирование хороших расширений» бесплатный?
Да — полный текст урока «Проектирование хороших расширений» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Проектирование хороших расширений»?
Применяйте лучшие практики и избегайте типичных ошибок. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Проектирование хороших расширений»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Определение методов расширения
- Расширение интерфейсов и обобщённых типов
- Выбор метода расширения
- Проектирование хороших расширений