Прочитав эту главу, вы научитесь:
• создавать метод, принимающий любое количество аргументов с использованием ключевого слова params;
• создавать метод, принимающий любое количество аргументов любого типа с использованием ключевого слова params в сочетании с типом объекта;
• объяснять, чем отличаются методы, принимающие массивы параметров, от методов, принимающих необязательные параметры.
Массивы параметров применяются при необходимости создания методов, принимающих в качестве параметров любое количество аргументов, возможно, относящихся к разным типам. Если вы знакомы с понятиями объектно-ориентированного программирования, то последнее предложение может вам сильно не понравиться, ведь при объектно-ориентированном подходе подобная задача решается путем определения перегружаемых методов. Но перегрузка не всегда оказывается самым рациональным подходом, особенно если нужно создать метод, действительно способный принимать разное количество параметров, каждый из которых при вызове метода может иметь свой, отличный от других тип. В этой главе описываются способы использования массивов параметров, предназначенные для разрешения подобных ситуаций.
Перегрузка — это техническое понятие для объявления двух и более методов с одним и тем же именем в одном и том же пространстве имен. Перегрузка метода хорошо подходит для тех случаев, когда нужно выполнить одно и то же действие в отношении аргументов различных типов. Классическим примером перегрузки в Microsoft Visual C# является метод Console.WriteLine. Этот метод может многократно перегружаться, позволяя вам передавать ему любой аргумент, относящийся к простым типам данных. В следующем примере кода показано несколько способов определения метода WriteLine в классе Console:
class Console
{
public static void WriteLine(Int32 value)
public static void WriteLine(Double value)
public static void WriteLine(Decimal value)
public static void WriteLine(Boolean value)
public static void WriteLine(String value)
...
}
ПРИМЕЧАНИЕ В документации по методу WriteLine в качестве его параметров используются структурные типы, определенные в пространстве имен System, а не псевдонимы, используемые в C# для этих типов. Например, перегружаемый метод, выводящий значение типа int, на самом деле получает в качестве параметра значение типа Int32. Список структурных типов и их отображений на C#-псевдонимы приведен в главе 9 «Создание типов значений с использованием перечислений и структур».
Как бы ни была полезна перегрузка, она не может подойти абсолютно для всех случаев. В частности, перегрузка не позволяет легко справиться с ситуацией, при которой тип параметров не изменяется, а вот их количество может изменяться. Что, к примеру, делать, если нужно вывести на консоль множество значений? Неужели придется предоставлять версию Console.WriteLine, способную принять два параметра в различных комбинациях, а также другие версии, способные принять три параметра и т.д.? Так ведь можно слишком быстро утомиться. И неужели вы так легко согласитесь с таким большим количеством дублей перегружаемых методов? Вряд ли. К счастью, есть способ написания метода, принимающего переменное число аргументов: можно воспользоваться массивом параметров, объявляемым с использованием ключевого слова params.
Чтобы понять, как params-массивы решают эту проблему, будет полезно разобраться с использованием обычных массивов и со слабыми сторонами этого подхода.
Предположим, нужно создать метод для определения минимального значения в наборе значений, переданных в качестве параметров. Один из способов предполагает использование массива. Например, чтобы определить наименьшее из нескольких int-значений, можно написать статический метод по имени Min с единственным параметром, представляющим собой массив из int-значений:
class Util
{
public static int Min(int[] paramList)
{
// Проверка предоставления вызывающим кодом хотя бы одного параметра.
// Если нет, выдача исключения ArgumentException, свидетельствующего о том,
// что невозможно найти наименьшее значение в пустом списке.
if (paramList == null || paramList.Length == 0)
{
throw new ArgumentException("Util.Min: not enough arguments");
}
// Установка текущего минимального значения, найденного в списке
// параметров, на первый элемент
int currentMin = paramList[0];
// Последовательное обращение к списку параметров с целью поиска любого
// значения, меньшего, чем то, что содержится в currentMin
foreach (int i in paramList)
{
// Если в цикле обнаруживается элемент, меньший, чем значение,
// содержащееся в currentMin, установка для currentMin этого значения
if (i < currentMin)
{
currentMin = i;
}
}
// В конце цикла в currentMin содержится значение наименьшего элемента
// из списка параметров, поэтому возвращается именно это значение.
return currentMin;
}
}
ПРИМЕЧАНИЕ Класс ArgumentException разработан специально для того, чтобы метод выдавал исключения, если предоставленные аргументы не отвечают требованиям метода.
Чтобы воспользоваться методом Min для поиска минимума среди двух int-переменных с именами first и second, можно создать следующий код:
int[] array = new int[2];
array[0] = first;
array[1] = second;
int min = Util.Min(array);
А для использования метода Min с целью поиска минимума среди трех int-переменных (с именами first, second и third) можно создать следующий код:
int[] array = new int[3];
array[0] = first;
array[1] = second;
array[2] = third;
int min = Util.Min(array);
Как видите, такое решение позволяет не создавать большое количество перегрузок, но за это приходится платить: вам следует написать дополнительный код для заполнения массива тем, что вы передаете методу. Разумеется, при желании можно воспользоваться безымянным массивом:
int min = Util.Min(new int[] {first, second, third});
Но главное здесь то, что вам все равно нужно создавать и заполнять массив, и синтаксис при этом может выглядеть не вполне понятным. Решение проблемы заключается в том, чтобы заставить компилятор написать часть этого кода за вас за счет использования в качестве параметра для метода Min массива параметров.
Используя params-массив, вы сможете передавать методу переменное количество аргументов. Этот массив обозначается при определении параметров метода с помощью ключевого слова params, которое служит модификатором массива параметров. Вот как, к примеру, выглядит еще один вариант метода Min, на этот раз с массивом параметров, объявленным в качестве params-массива:
class Util
{
public static int Min(params int[] paramList)
{
// код точно такой же, как и ранее использованный
}
}
Эффект от применения ключевого слова params в отношении метода Min заключается в том, что вы получаете возможность вызывать метод, используя любое количество целочисленных аргументов, не заботясь о создании массива. Например, для нахождения минимума из двух целочисленных значений можно просто написать следующий код:
int min = Util.Min(first, second);
компилятор преобразует этот вызов в код, похожий на следующий:
int[] array = new int[2];
array[0] = first;
array[1] = second;
int min = Util.Min(array);
Для нахождения минимума из трех целочисленных значений следует написать код, показанный далее, который также преобразуется компилятором в соответствующий код, использующий массив:
int min = Util.Min(first, second, third);
Оба вызова метода Min (один с двумя, а один с тремя аргументами) обращаются к одному и тому же методу Min, объявленному с ключевым словом params. И как вы уже могли догадаться, этот метод Min можно вызывать с любым количеством int-аргументов. Компилятор просто подсчитывает количество int-аргументов, создает int-массив такого же размера, заполняет массив аргументами, а затем вызывает метод, передавая ему единственный массив параметров.
ПРИМЕЧАНИЕ При наличии опыта программирования на C или на C++ вы можете распознать в params безопасный по отношению к типам эквивалент макроса varargs из заголовочного файла stdarg.h. В языке Java есть также средство varargs, работающее примерно так же, как ключевое слово params в C#.
По поводу params-массивов нужно отметить следующий ряд обстоятельств.
• Ключевое слово params нельзя использовать с многомерными массивами. Код следующего примера компиляцию не пройдет:
// ошибка в ходе компиляции
public static int Min(params int[,] table)
...
• Метод, основанный исключительно на применении ключевого слова params, перегрузить невозможно. Как показано в следующем примере, ключевое слово params не является частью сигнатуры метода. В данном случае в контексте вызывающего кода компилятор не сможет отличить эти методы друг от друга:
// ошибка в ходе компиляции: продублированное объявление
public static int Min(int[] paramList)
...
public static int Min(params int[] paramList)
...
• При использовании params-массивов нельзя указывать модификатор ref или out:
// ошибки в ходе компиляции
public static int Min(ref params int[] paramList)
...
public static int Min(out params int[] paramList)
...
• Массив params должен быть последним параметром. (Это означает, что у вас на один метод может быть только один params-массив.) Изучите данный пример:
// ошибка в ходе компиляции
public static int Min(params int[] paramList, int i)
...
• Метод, в объявлении которого не используется ключевое слово params, всегда имеет приоритет над params-методом. Это означает, что создать для наиболее распространенных случаев перегружаемую версию метода все же можно:
public static int Min(int leftHandSide, int rightHandSide)
...
public static int Min(params int[] paramList)
...
Первая версия метода Min используется, когда он вызывается с использованием двух int-аргументов. Вторая версия используется при предоставлении другого количества int-аргументов. Сюда же включается и случай вызова метода без аргументов. Добавление метода, не использующего params-массив, может стать полезным приемом оптимизации, поскольку компилятору не придется создавать и заполнять слишком большое количество массивов.
Массив параметров int-типа весьма удобен. С его помощью в вызове метода можно передать любое количество int-аргументов. А что делать, если изменяется не только количество аргументов, но и их тип? В C# имеется способ решения и этой задачи. Технология основана на том, что object является основой всех классов и компилятор может генерировать код, преобразующий типы значений (не являющиеся классами) в объекты с использованием упаковки (boxing), согласно описанию, приведенному в главе 8 «Основные сведения о значениях и ссылках». Массив параметров типа object можно использовать при объявлении метода, принимающего любое количество object-аргументов, что позволяет передаваемым аргументам иметь отношение к любому типу. Рассмотрим следующий пример:
class Black
{
public static void Hole(params object[] paramList)
...
}
Я назвал этот метод Black.Hole (черная дыра), потому что им поглощаются любые аргументы.
• Методу можно вообще не передавать никакие аргументы, в случае чего компилятор передаст object-массив, имеющий длину 0:
Black.Hole();
// преобразуется в Black.Hole(new object[0]);
• Метод Black.Hole можно вызвать, передав ему в качестве аргумента null. Массив относится к ссылочному типу, поэтому инициализировать его значением null не запрещается:
Black.Hole(null);
• Методу Black.Hole можно передать уже существующий массив. Иными словами, массив, который обычно создается компилятором, можно создать вручную:
object[] array = new object[2];
array[0] = "forty two";
array[1] = 42;
Black.Hole(array);
• Методу Black.Hole можно передать аргументы разных типов, и эти аргументы будут автоматически помещены в object-массив:
Black.Hole("forty two", 42);
//преобразуется в Black.Hole(new object[]{"forty two", 42});
Метод Console.WriteLine
Класс Console содержит множество перегружаемых версий метода WriteLine. Одна из таких версий имеет следующий вид:
public static void WriteLine(string format, params object[] arg);
Хотя строковая интерполяция практически сделала эту версию метода WriteLine излишней, в предыдущих редакциях языка C# она использовалась довольно часто. Эта перегрузка позволяла методу WriteLine поддерживать аргумент форматирования строки, содержащий числовые поля для заполнения, каждое из которых могло заменяться в ходе выполнения программы переменной любого типа, указанной в списке параметров (поле для замены {i} заменялось i-той переменной, указываемой в последующем списке). Рассмотрим пример вызова этого метода (переменные fname и lname являются строками, mi относится к типу char, а age — к int):
Console.WriteLine("Forename:{0}, Middle Initial:{1}, Last name:{2},
Age:{3}", fname, mi, lname, age);
Компилятор превращает этот вызов в следующий:
Console.WriteLine("Forename:{0}, Middle Initial:{1}, Last name:{2},
Age:{3}", new object[4]{fname, mi, lname, age});
В следующем упражнении будет создан и протестирован статический метод по имени Sum. Он предназначен для вычисления суммы переданного ему переменного количества int-аргументов и возвращения результата в виде int-значения. Делаться это будет путем создания Sum, принимающего параметр params int[]. Будут также выполнены две проверки параметра params, чтобы убедиться, что метод Sum абсолютно надежен. Затем с целью тестирования метод Sum будет вызываться с разным количеством различных аргументов.
Откройте в среде Microsoft Visual Studio 2015 проект ParamsArray, который находится в папке \Microsoft Press\VCSBS\Chapter 11\ParamsArray вашей папки документов.
В проекте ParamsArray в файле Program.cs содержится класс Program, включающий в себя метод среды doWork, уже встречавшийся в предыдущих главах. Метод Sum будет создан как статический метод другого класса по имени Util (сокращение от слова utility), который вы добавите к проекту.
Щелкните правой кнопкой мыши в окне обозревателя решений на проекте ParamsArray, который находится в решении ParamsArray, найдите пункт Добавить (Add), а затем щелкните на пункте Класс (Class).
В средней панели диалогового окна Добавить новый элемент – ParamsArray щелкните на пункте Класс. В поле Имя (Name) наберите строку Util.cs, а затем щелкните на кнопке Добавить (Add). В результате этого будет создан и добавлен к проекту файл Util.cs. В нем содержится пустой класс по имени Util, находящийся в пространстве имен ParamsArray.
Добавьте к классу Util открытый статический метод по имени Sum. Этот метод должен возвращать int-значение и принимать params-массив из int-значений по имени paramList. Он должен иметь следующий вид:
public static int Sum(params int[] paramList)
{
}
Первым шагом на пути создания метода Sum станет проверка параметра paramList. Кроме того что он должен содержать допустимый набор целых чисел, он также может содержать значение null или быть массивом нулевой длины. В обоих этих случаях суммы вычислить сложно, поэтому лучше будет выдать исключение ArgumentException. (Конечно, можно согласиться с тем, что сумма целых чисел в массиве нулевой длины равна нулю, но в данном примере такая ситуация будет расцениваться как исключительная.)
Добавьте к методу Sum код, выделенный жирным шрифтом. Этот код выдает исключение ArgumentException, если paramList имеет значение null. Теперь метод Sum должен приобрести следующий вид:
public static int Sum(params int[] paramList)
{
if (paramList == null)
{
throw new ArgumentException("Util.Sum: null parameter list");
}
}
Добавьте к методу Sum код, выделенный жирным шрифтом и предназначенный для выдачи исключения ArgumentException, если длина списка параметров равна нулю:
public static int Sum(params int[] paramList)
{
if (paramList == null)
{
throw new ArgumentException("Util.Sum: null parameter list");
}
if (paramList.Length == 0)
{
throw new ArgumentException("Util.Sum: empty parameter list");
}
}
Если массив пройдет эти два теста, то нужно будет сложить все элементы массива. Для этого можно воспользоваться инструкцией foreach; кроме этого, понадобится локальная переменная для хранения возрастающего общего значения.
Сразу же после кода, созданного на предыдущих этапах, объявите целочисленную переменную по имени sumTotal и инициализируйте ее значением 0:
public static int Sum(params int[] paramList)
{
...
if (paramList.Length == 0)
{
throw new ArgumentException("Util.Sum: empty parameter list");
}
int sumTotal = 0;
}
Добавьте к методу Sum инструкцию foreach, позволяющую получить последовательный доступ к элементам массива paramList. В теле этого цикла foreach нужно прибавить значение каждого элемента массива к переменной sumTotal. В конце метода нужно путем использования инструкции return возвратить значение переменной sumTotal. Все новые добавления выделены жирным шрифтом:
public static int Sum(params int[] paramList)
{
...
int sumTotal = 0;
foreach (int i in paramList)
{
sumTotal += i;
}
return sumTotal;
}
Щелкните в меню Сборка на пункте Собрать решение, после чего убедитесь, что сборка прошла без ошибок.
Выведите в окно редактора файл Program.cs и замените в методе doWork комментарий // TODO: следующей инструкцией:
Console.WriteLine(Util.Sum(null));
Щелкните в меню Отладка на пункте Начать отладку. Произойдут сборка и запуск программы, после чего на консоль будет выведено следующее сообщение:
Exception: Util.Sum: null parameter list
Тем самым вы получите подтверждение работоспособности первой имеющейся в методе проверки. Нажмите Ввод, чтобы закрыть программу и вернуться в среду Visual Studio 2015.
Замените в окне редактора вызов Console.WriteLine, находящийся в методе doWork, следующим вызовом:
Console.WriteLine(Util.Sum());
На этот раз метод вызывается без аргументов. Компилятор транслирует пустой список аргументов в пустой массив. Щелкните в меню Отладка на пункте Запуск без отладки. Произойдут сборка и запуск программы, после чего на консоль будет выведено следующее сообщение:
Exception: Util.Sum: empty parameter list
Тем самым вы получите подтверждение работоспособности второй имеющейся в методе проверки. Нажмите Ввод, чтобы закрыть программу и вернуться в среду Visual Studio 2015.
Замените в окне редактора вызов Console.WriteLine, находящийся в методе doWork, следующим вызовом:
Console.WriteLine(Util.Sum(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что программа прошла сборку, запуск и вывела на экран консоли значение 55.
Нажмите Ввод, чтобы закрыть приложение и вернуться в среду Visual Studio 2015.
В главе 3 «Создание методов и применение областей видимости» было показано, как определяются методы, получающие необязательные параметры. На первый взгляд может показаться, что функционально методы, использующие массивы параметров, и методы, получающие необязательные параметры, чем-то перекрывают друг друга. Но между ними есть принципиальные различия.
• Метод, получающий дополнительные параметры, все же располагает фиксированным списком параметров, и передать ему произвольный список аргументов невозможно. Компилятор создает код, который перед запуском метода вставляет в стек значения по умолчанию для любого пропущенного аргумента, и метод остается в неведении о том, какие аргументы предоставлены вызывающим его кодом, а какие создаются компилятором из значений, предоставляемых по умолчанию.
• Метод, эффективно использующий массив параметров, имеет совершенно произвольный список параметров, и ни один из них не имеет значения по умолчанию. Более того, метод может с абсолютной точностью определить, сколько именно аргументов предоставил вызывающий его код.
Обычно массивы параметров используются для методов, способных принять любое количество параметров (включая и их полное отсутствие), а необязательные параметры используются, только если вызывающий код неудобно заставлять заниматься предоставлением аргумента для каждого параметра.
Стоит задуматься и еще об одной ситуации. Если определяется метод, получающий список параметров, и предоставляется его перегружаемая версия, получающая необязательные параметры, то не всегда очевидно, какая из версий будет вызвана, если список аргументов в вызывающей инструкции подходит для обеих сигнатур метода. Этот сценарий будет исследован в заключительном упражнении данной главы.
Вернитесь в среду Visual Studio 2015 к решению ParamsArray и выведите в окно редактора файл Util.cs. Добавьте к началу метода Sum в классе Util показанную жирным шрифтом инструкцию Console.WriteLine:
public static int Sum(params int[] paramList)
{
Console.WriteLine("Using parameter list");
...
}
Добавьте к классу Util еще одну реализацию метода Sum. Эта версия будет получать четыре необязательных int-параметра, для каждого из которых по умолчанию устанавливается нулевое значение. В теле метода присутствует инструкция вывода сообщения «Using optional parameters» («Использование необязательных параметров»), после чего вычисляется и возвращается сумма четырех параметров. Окончательный вариант этого метода выделен в следующем примере жирным шрифтом:
class Util
{
...
public static int Sum(int param1 = 0, int param2 = 0, int param3 = 0,
int param4 = 0)
{
Console.WriteLine("Using optional parameters");
int sumTotal = param1 + param2 + param3 + param4;
return sumTotal;
}
}
Выведите в окно редактора файл Program.cs. Закомментируйте в методе doWork имеющийся код и добавьте к методу следующую инструкцию:
Console.WriteLine(Util.Sum(2, 4, 6, 8));
Эта инструкция вызывает метод Sum, передавая ему четыре int-параметра. Этот вызов подходит обоим перегружаемым вариантам метода Sum.
Щелкните в меню Отладка на пункте Запуск без отладки, чтобы выполнить сборку и запуск программы, после чего на консоль будут выведены следующие сообщения:
Using optional parameters
20
В данном случае компилятор создал код, который вызывает метод, принимающий четыре необязательных параметра. Эта версия метода наилучшим образом подходит к вызову метода. Нажмите Ввод и вернитесь в среду Visual Studio.
Измените в методе doWork инструкцию, вызывающую метод Sum, удалив последний аргумент (8):
Console.WriteLine(Util.Sum(2, 4, 6));
Щелкните в меню Отладка на пункте Запуск без отладки, чтобы выполнить сборку и запуск приложения, после чего на консоль будут выведены следующие сообщения:
Using optional parameters
12
Компилятор по-прежнему создает код, который вызывает метод, принимающий четыре необязательных параметра, даже при том что его сигнатура не в полной мере подходит к вызову. Когда компилятор C# стоит перед выбором между методом, принимающим необязательные параметры, и методом, принимающим список параметров, он отдает предпочтение первому варианту. Нажмите Ввод и вернитесь в среду Visual Studio.
Еще раз измените в методе doWork ту инструкцию, которая вызывает метод Sum, добавив к ней еще два дополнительных аргумента:
Console.WriteLine(Util.Sum(2, 4, 6, 8, 10));
Щелкните в меню Отладка на пункте Запуск без отладки, чтобы выполнить сборку и запуск приложения, после чего на консоль будут выведены следующие сообщения:
Using parameter list
30
На этот раз предоставлено больше аргументов, чем указано в методе, принимающем необязательные параметры, поэтому компилятор создает код, который вызывает метод, принимающий массив параметров. Нажмите Ввод и вернитесь в среду Visual Studio.
В этой главе вы узнали, как используются массивы параметров для определения метода, который может принимать любое количество аргументов. Вы также увидели, как используется params-массив элементов типа object для создания метода, принимающего любое количество аргументов любого типа. Кроме того, вы увидели, как компилятор разбирается с вызовами метода, когда у него есть выбор между вызовом варианта метода, принимающего массив параметров, и вызовом варианта метода, принимающего необязательные параметры.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 12 «Работа с наследованием».
Если вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Создать метод, принимающий любое количество аргументов заданного типа | Создайте метод, чьим параметром будет params-массив заданного типа. Например, метод, принимающий любое количество bool-аргументов, объявляется следующим образом: someType Method(params bool[] flags) { ... } |
Создать метод, принимающий любое количество аргументов любого типа | Создайте метод, чьим параметром будет params-массив, с элементами типа object, например: someType Method(params object[] paramList) { ... } |