Главная страница   /   4.4. Использование методов расширения (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

4.4. Использование методов расширения

Методы расширения представляют собой удобный способ добавления методов в классы, которые вам не принадлежат и поэтому не могут быть изменены напрямую. В листинге 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