Использование методов расширения
Методы расширения представляют собой удобный способ добавления методов в классы, которые вам не принадлежат и поэтому не могут быть изменены напрямую. В листинге 4-11 показан класс ShoppingCart
, который мы добавили в папку Models
и который представляет собой коллекцию объектов Product
.
Листинг 4-11: Класс ShoppingCart
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace LanguageFeatures.Models
{
public class ShoppingCart
{
public List<Product> Products { get; set; }
}
}
Это очень простой класс, который действует как оболочка вокруг List
объектов Product
( нам нужен простой класс для данного примера). Предположим, что нам нужна возможность определять общую стоимость объектов Product
в классе ShoppingCart
, но мы не можем изменить сам класс, например, потому что он получен от третьей стороны и у нас нет исходного кода. К счастью, мы можем использовать метод расширения для получения необходимого функционала. В листинге 4-12 показан класс MyExtensionMethods
, который мы также добавили в папку Models
.
Листинг 4-12: Определение метода расширения
namespace LanguageFeatures.Models
{
public static class MyExtensionMethods
{
public static decimal TotalPrices(this ShoppingCart cartParam)
{
decimal total = 0;
foreach (Product prod in cartParam.Products)
{
total += prod.Price;
}
return total;
}
}
}
Ключевое слово this
перед первым параметром отмечает TotalPrices
как расширенный метод. Первый параметр говорит .NET, к какому классу можно применять метод расширения, в нашем случае к ShoppingCart
. Мы можем обратиться к экземпляру ShoppingCart
, к которому был применен расширенный метод, с помощью параметра cartParam
. Наш метод перечисляет объекты Product
в ShoppingCart
и возвращает сумму свойства Product.Price
. В листинге 4-13 показано, как мы применяем метод расширения в новом методе действия UseExtension
, который мы добавили контроллеру Home
.
Примечание
Методы расширения не позволяют обойти правила доступа, которые определяют классы для своих методов, полей и свойств. Вы можете расширить функционал класса с помощью расширенного метода, но только используя членов класса, к которым у вас так или иначе есть доступ.
Листинг 4-13: Применение метода расширения
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace LanguageFeatures.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Navigate to a URL to show an example";
}
// ...другие методы действия опущены для краткости...
public ViewResult UseExtension()
{
// создание и заполнение ShoppingCart
ShoppingCart cart = new ShoppingCart
{
Products = new List<Product> {
new Product {Name = "Kayak", Price = 275M},
new Product {Name = "Lifejacket", Price = 48.95M},
new Product {Name = "Soccer ball", Price = 19.50M},
new Product {Name = "Corner flag", Price = 34.95M}
}
};
// получение общей стоимости продуктов в корзине
decimal cartTotal = cart.TotalPrices();
return View("Result",
(object)String.Format("Total: {0:c}", cartTotal));
}
}
}
Ключевым выражением является вот это:
...
decimal cartTotal = cart.TotalPrices();
...
Как вы видите, мы называем метод TotalPrices
для объекта ShoppingCart
, как будто это часть класса ShoppingCart
, даже если это расширенный метод, определенный другим классом. .NET найдет расширенные методы, если они находятся в рамках текущего класса, что обозначает, что они являются частью одного и того же пространства имен или находятся в пространстве имен, которое является предметом выражения using
. Вот результат использования метода действия UseExtension
:
Total: $378.40
Применение методов расширения к интерфейсу
Мы также можем создать расширенные методы, применяемые к интерфейсу, который позволяет вызвать метод расширения для всех классов, реализующих этот интерфейс. В листинге 4-14 показан класс ShoppingCart
, обновленный для реализации интерфейса IEnumerable<Product>
.
Листинг 4-14: Реализация интерфейса в классе ShoppingCart
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace LanguageFeatures.Models
{
public class ShoppingCart : IEnumerable<Product>
{
public List<Product> Products { get; set; }
public IEnumerator<Product> GetEnumerator()
{
return Products.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Теперь мы можем обновить наш метод расширения, чтобы он работал с IEnumerable<Product>
, как показано в листинге 4-15.
Листинг 4-15: Метод расширения, который работает с интерфейсом
using System.Collections.Generic;
namespace LanguageFeatures.Models
{
public static class MyExtensionMethods
{
public static decimal TotalPrices(this IEnumerable<Product> productEnum)
{
decimal total = 0;
foreach (Product prod in productEnum)
{
total += prod.Price;
}
return total;
}
}
}
Первый тип параметра изменился на IEnumerable<Product>
, это обозначает, что цикл foreach
в теле метода работает непосредственно с объектами Product
. В противном случае метод расширения остается неизменным. Переход на интерфейс означает, что мы можем рассчитать общую стоимость объектов Product
, перечисленных любым IEnumerable<Product>
, который включает в себя экземпляры ShoppingCart
, а также массивы объектов Product
, как показано в листинге 4-16.
Листинг 4-16: Применение метода расширения к различным реализациям одного интерфейса
using LanguageFeatures.Models;
using System;
using System.Collections.Generic;
using LanguageFeatures.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace LanguageFeatures.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Navigate to a URL to show an example";
}
// ...другие метода действия опущены для краткости...
public ViewResult UseExtensionEnumerable()
{
IEnumerable<Product> products = new ShoppingCart
{
Products = new List<Product> {
new Product {Name = "Kayak", Price = 275M},
new Product {Name = "Lifejacket", Price = 48.95M},
new Product {Name = "Soccer ball", Price = 19.50M},
new Product {Name = "Corner flag", Price = 34.95M}
}
};
// создание и заполнение массива объектов Product
Product[] productArray = {
new Product {Name = "Kayak", Price = 275M},
new Product {Name = "Lifejacket", Price = 48.95M},
new Product {Name = "Soccer ball", Price = 19.50M},
new Product {Name = "Corner flag", Price = 34.95M}
};
// получение общей стоимости продуктов в корзине
decimal cartTotal = products.TotalPrices();
decimal arrayTotal = productArray.TotalPrices();
return View("Result",
(object)String.Format("Cart Total: {0}, Array Total: {1}",
cartTotal, arrayTotal));
}
}
}
Примечание
Способ, которым C# массивы реализуют интерфейс
IEnumerable<T>
, немного необычен. Вы не найдете его в списке реализуемых интерфейсов в документации MSDN. Эта поддержка обрабатывается компилятором, поэтому данный код для более ранних версий C# по-прежнему компилируется. Странно, но это так. Мы могли бы использовать другой класс коллекции в этом примере, но мы хотели показать наши знания о темных углах спецификации C#. Также странно, но это так.
Если вы запустите проект и нацелитесь на метод действия, вы увидите следующие результаты, которые показывают, что мы получим тот же результат от расширенного метода, независимо от того, как собраны объекты Product
:
Cart Total: 378.40, Array Total: 378.40
Создание фильтрующих методов расширения
Последнее, что мы хотим рассказать вам о методах расширения, это то, что они могут быть использованы для фильтрации объектов коллекции. Расширенный метод, который работает с IEnumerable<T>
и который также возвращает IEnumerable<T>
, может использовать ключевое слово yield
, чтобы применить критерии выбора элементов в исходных данных для получения сокращенного набора результатов. В листинге 4-17 показан такой метод, который мы добавили в класс MyExtensionMethods
.
Листинг 4-17: Фильтрующий расширенный метод
using System.Collections.Generic;
namespace LanguageFeatures.Models
{
public static class MyExtensionMethods
{
public static decimal TotalPrices(this IEnumerable<Product> productEnum)
{
decimal total = 0;
foreach (Product prod in productEnum)
{
total += prod.Price;
}
return total;
}
public static IEnumerable<Product> FilterByCategory(
this IEnumerable<Product> productEnum, string categoryParam)
{
foreach (Product prod in productEnum)
{
if (prod.Category == categoryParam)
{
yield return prod;
}
}
}
}
}
Этот метод расширения, названный FilterByCategory
, принимает дополнительный параметр, который позволяет нам вводить условия фильтрации, когда мы вызываем метод. Те объекты Product
, свойство Category
которых соответствует параметру, возвращаются в результате использования IEnumerable<Product>
, а те, у которых не соответствует – отбрасываются. В листинге 4-18 показано, как используется этот метод.
Листинг 4-18: Использование фильтрующего расширенного метода
using LanguageFeatures.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace LanguageFeatures.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Navigate to a URL to show an example";
}
// ... другие методы действия опущены для краткости ...
public ViewResult UseFilterExtensionMethod()
{
IEnumerable<Product> products = new ShoppingCart
{
Products = new List<Product> {
new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
new Product {Name = "Lifejacket", Category = "Watersports",
Price = 48.95M},
new Product {Name = "Soccer ball", Category = "Soccer",
Price = 19.50M},
new Product {Name = "Corner flag", Category = "Soccer",
Price = 34.95M}
}
};
decimal total = 0;
foreach (Product prod in products.FilterByCategory("Soccer"))
{
total += prod.Price;
}
return View("Result", (object)String.Format("Total: {0}", total));
}
}
}
Когда мы вызываем метод FilterByCategory
для ShoppingCart
, возвращаются только те объекты Product
, которые находятся в категории Soccer
. Если вы запустите проект и будете использовать метод действия UseFilterExtensionMethod
, вы увидите следующий результат, который является общей стоимостью единиц продукции в категории Soccer
:
Total: 54.45