Изображение для статьи создано при помощи нейросети Kandinsky
Доброго времени суток, друзья.
В этой статье мы рассмотрим с вами на примерах применение модификатора params в объявлениях методов в программе на C#.
Этот модификатор бывает полезен, когда вам необходимо вызывать метод с различным количеством аргументов.
При указании модификатора params нужно учитывать следующее:
- никакие другие параметры для метода не разрешены после параметра с модификатором params, т.е. параметр с указанием params должен всегда быть последним в списке параметров метода
- если тип для params параметра не является одномерным массивом, то возникнет ошибка компилятора (подробнее про ошибку компиляции можно прочитать в документации)
- перед параметром с модификатором params вы можете при необходимости указывать другие параметры метода с нужными вам типами данных
- параметр с модификатором params может быть и единственным параметром метода, если другие обязательные параметры для метода не требуются
Все рассмотренные ниже в статье примеры я подготовил в новом консольном приложении (в среде Microsoft Visual Studio выбирал тип проекта: "Консольное приложение (Майкрософт)") и назвал его ParamsModifierExample. В конце статьи я приложу ссылку на пример готового проекта, и вы можете просто скачать его и запустить в своей среде разработки.
К примеру, рассмотрим такой статический метод PrintStrings, который умеет распечатывать на консоль переданные ему строки:
static void PrintStrings(params string[] strings) {
if (strings == null) {
return;
}
foreach (var str in strings) {
Console.WriteLine(str);
}
}
Как видим, в этом методе мы сразу указали параметр strings, представляющий собой одномерный массив строк и указали модификатор params для этого параметра.
Теперь посмотрим на возможные варианты вызова этого метода с различными комбинациями аргументов.
Во-первых, мы можем вызвать такой метод, передав ему несколько строк, разделённых через запятую (в примере ниже передаются строковые литералы, но это не обязательно, вы можете передавать и строковые переменные):
Console.WriteLine(">>> Результат вызова PrintStrings(\"раз\", \"два\", \"три\"):");
PrintStrings("раз", "два", "три"); // передаем требуемое количество строковых аргументов в вызове
При запуске на консоль будет выведено:
>>> Результат вызова PrintStrings("раз", "два", "три"):
раз
два
три
Во-вторых, такой метод можно вызвать, вообще не передавая ему аргументов:
Console.WriteLine(">>> Результат вызова PrintStrings():");
PrintStrings(); // вызов без аргументов также возможен
При запуске этого варианта сам метод ничего не выведет на консоль, поскольку длина массива для параметра strings метода равна 0. Мы увидим только:
>>> Результат вызова PrintStrings():
В-третьих, мы технически можем передать в метод динамически созданный массив строк (хотя этот способ менее удобен, чем первый вариант с перечислением строк через запятую, что мы рассмотрели выше, кроме этого с установленным плагином Sonar Lint for Visual Studio 2022 вы получите предупреждение в вашем редакторе кода, с описанием того, что это несоответствующий вариант вызова, т.е. он считается "запахом", или Code Smell, и его следует избегать):
Console.WriteLine(">>> Результат вызова PrintStrings(new string[] { \"раз\", \"два\", \"три\", \"четыре\" }):");
PrintStrings(new string[] { "раз", "два", "три", "четыре" }); // передаем динамически созданный массив строк
При запуске этого варианта мы увидим в консоли следующее:
>>> Результат вызова PrintStrings(new string[] { "раз", "два", "три", "четыре" }):
раз
два
три
четыре
В-четвёртых, мы также - чисто технически - можем передать в метод даже null, но нужно иметь в виду, что это тоже приведёт к предупреждению CS8625 в редакторе кода, хотя код и скомпилируется:
Console.WriteLine(">>> Результат вызова PrintStrings(null):");
PrintStrings(null); // передать null в метод также технически возможно
Здесь я отмечу, что в примере нашего метода PrintStrings мы в самом начале делаем проверку параметра strings на null именно для того, чтобы код метода не "упал" при выполнении программы с исключением NullReferenceException. Ради эксперимента можете закомментировать проверку
if (strings == null) {
return;
}
и посмотреть, что произойдет при выполнении этого варианта вызова метода.
Если вы гарантированно знаете, что в метод никогда и ни при каких условиях не будет передаваться null, то эта проверка может быть излишней.
Напоследок скажу, что мы не сможем вызвать наш метод PrintStrings, передавая ему в качестве аргументов, например, числа, т.е. подобный вызов будет запрещен компилятором: PrintStrings(1, 2, 3).
Теперь давайте посмотрим на другой метод PrintObjects. Он будет очень похож на рассмотренный выше PrintStrings, но на этот раз входным параметром будет являться одномерный массив объектов (тип данных object[]), также с указанием модификатора params:
static void PrintObjects(params object[] objects) {
if (objects == null) {
return;
}
foreach (var obj in objects) {
Console.WriteLine(obj);
}
}
На этот раз мы можем передавать в метод как строки, так и, например, числа или литералы типа данных bool (т.е. true и false):
Console.WriteLine(">>> Результат вызова PrintObjects(1, 2, 3):");
PrintObjects(1, 2, 3);
Console.WriteLine(">>> Результат вызова PrintObjects(\"1\", \"2\", \"3\"):");
PrintObjects("1", "2", "3");
Console.WriteLine(">>> Результат вызова PrintObjects(true, false):");
PrintObjects(true, false);
Console.WriteLine(">>> Результат вызова PrintObjects(new string[] { \"1\", \"2\", \"3\" }, new int[] { 1, 2, 3 }):");
PrintObjects(new string[] { "1", "2", "3" }, new int[] { 1, 2, 3 });
В последнем варианте вызова, как видите, мы вообще передали через запятую два разных динамически созданных массива строк и чисел (в реальных программах такое вряд ли понадобится, но технически метод возможно вызвать подобным образом).
При запуске примера выше на экране консоли отобразится следующее:
>>> Результат вызова PrintObjects(1, 2, 3):
1
2
3
>>> Результат вызова PrintObjects("1", "2", "3"):
1
2
3
>>> Результат вызова PrintObjects(true, false):
True
False
>>> Результат вызова PrintObjects(new string[] { "1", "2", "3" }, new int[] { 1, 2, 3 }):
System.String[]
System.Int32[]
Как видите, для последнего варианта вызова вместо строк и чисел в консоли вывелось System.String[] и System.Int32[]. Это происходит из-за того, что тело цикла foreach нашего метода PrintObjects выполняется всего два раза (по количеству переданных массивов в вызове), первый раз - в переменной obj цикла будет находиться массив string[3] с тремя нашими строками, а второй раз - массив int[3] с тремя переданными числами 1, 2 и 3. Поэтому при желании вывести реальное содержимое этих массивов на консоль, нужно было бы предусмотреть дополнительную логику внутри цикла, например:
foreach (var obj in objects) {
if (obj is IEnumerable<int> enumerableOfInts) {
foreach (var num in enumerableOfInts) {
Console.WriteLine(num);
}
} else if (obj is IEnumerable<string> enumerableOfStrings) {
foreach (var str in enumerableOfStrings) {
Console.WriteLine(str);
}
} else {
Console.WriteLine(obj);
}
}
Давайте теперь ещё попробуем передать в метод PrintObjects какие-нибудь экземпляры классов и структур и посмотреть, как будет вести себя метод.
Создадим в отдельных файлах SimpleClass.cs и Coordinate3D.cs следующие класс и структуру, соответственно:
SimpleClass.cs:
namespace ParamsModifierExample {
/// <summary>
/// Простой пример класса с единственным свойством Property
/// </summary>
internal class SimpleClass {
public string? Property { get; set; }
public SimpleClass() { }
public SimpleClass(string property) {
Property = property;
}
public override string ToString() {
return "SimpleClass@" + GetHashCode() + "{\r\n" +
$"\tProperty: \"{Property}\"" +
"\r\n}";
}
public override bool Equals(object? obj) {
return obj is SimpleClass @class &&
Property == @class.Property;
}
public override int GetHashCode() {
return HashCode.Combine(Property);
}
}
}
Coordinate3D.cs:
using System.Diagnostics.CodeAnalysis;
namespace ParamsModifierExample {
/// <summary>
/// Пример структуры для описания точки с координатами X, Y, Z в трёхмерном пространстве
/// </summary>
internal struct Coordinate3D {
private float x;
private float y;
private float z;
public float X { readonly get => x; set => x = value; }
public float Y { readonly get => y; set => y = value; }
public float Z { readonly get => z; set => z = value; }
public Coordinate3D(float x, float y, float z) {
X = x; Y = y; Z = z;
}
public override readonly string ToString() => $"{{X={X}, Y={Y}, Z={Z}}}";
public static Coordinate3D operator -(Coordinate3D first, Coordinate3D second) => new Coordinate3D(first.X - second.X, first.Y - second.Y, first.Z - second.Z);
public static Coordinate3D operator +(Coordinate3D first, Coordinate3D second) => new Coordinate3D(first.X + second.X, first.Y + second.Y, first.Z + second.Z);
public static bool operator ==(Coordinate3D first, Coordinate3D second) => first.X == second.X && first.Y == second.Y && first.Z == second.Z;
public static bool operator !=(Coordinate3D first, Coordinate3D second) => !(first == second);
public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is Coordinate3D && Equals((Coordinate3D)obj);
public readonly bool Equals(Coordinate3D other) => this == other;
public override readonly int GetHashCode() => HashCode.Combine(X, Y, Z);
}
}
Как видим, класс просто содержит единственное открытое (public) свойство Property с типом данных string, два варианта конструктора, а также переопределяет методы ToString, Equals и GetHashCode.
Структура Coordinate3D описывает некоторую точку 3D-пространства и содержит три закрытых поля x, y, z, соответствующие открытые свойства X, Y, Z для этих полей, а также содержит переопределения методов ToString, Equals, GetHashCode и перегрузку для операторов -, +, ==, !=.
Попробуем теперь передать экземпляры этого класса и структуры в наш метод PrintObjects:
Console.WriteLine(">>> Результат вызова PrintObjects(new SimpleClass(\"свойство1\"), new SimpleClass(\"свойство2\")):");
PrintObjects(new SimpleClass("свойство1"), new SimpleClass("свойство2"), new SimpleClass());
Console.WriteLine(">>> Результат вызова PrintObjects(new Coordinate3D(1.0f, .2f, 1.5f), new Coordinate3D(), new Coordinate3D(4, 4, 4) - new Coordinate3D(1, 2, 3)):");
PrintObjects(new Coordinate3D(1.0f, .2f, 1.5f), new Coordinate3D(), new Coordinate3D(4, 4, 4) - new Coordinate3D(1, 2, 3));
И посмотрим, что будет выведено на экран консоли:
>>> Результат вызова PrintObjects(new SimpleClass("свойство1"), new SimpleClass("свойство2")):
SimpleClass@-1594386805{
Property: "свойство1"
}
SimpleClass@-1848808240{
Property: "свойство2"
}
SimpleClass@1502218669{
Property: ""
}
>>> Результат вызова PrintObjects(new Coordinate3D(1.0f, .2f, 1.5f), new Coordinate3D(), new Coordinate3D(4, 4, 4) - new Coordinate3D(1, 2, 3)):
{X=1, Y=0,2, Z=1,5}
{X=0, Y=0, Z=0}
{X=3, Y=2, Z=1}
Итак, мы видим, что за счёт переопределения метода ToString и для класса, и для структуры, метод PrintObjects распечатал в цикле на экране консоли текстовое представление экземпляров класса и структуры.
Давайте теперь ещё посмотрим, как работать с модификатором params, если он применяется не к единственному параметру метода, а перед ним есть ещё какие-то параметры.
Определим следующий метод PrintTwoOrMoreNumbers:
static void PrintTwoOrMoreNumbers(int mandatoryFirst, int mandatorySecond, params int[] optionalOtherNumbers) {
Console.WriteLine($"mandatoryFirst: {mandatoryFirst}");
Console.WriteLine($"mandatorySecond: {mandatorySecond}");
if (optionalOtherNumbers == null) {
return;
}
Console.WriteLine("optionalOtherNumbers: ");
foreach (var otherNumber in optionalOtherNumbers) {
Console.Write(otherNumber);
Console.Write(" ");
}
Console.WriteLine();
}
Как следует из его названия, он печатает на экран консоли два числа или больше. Видим также, что параметр optionalOtherNumbers, к которому применён модификатор params, идёт последним в методе. Первые же два параметра mandatoryFirst и mandatorySecond, в отличие от последнего, представляют собой обязательные параметры метода, т.е. при вызове метода в них обязательно нужно что-то передать.
Рассмотрим примеры вызовов метода:
Console.WriteLine(">>> Результат вызова PrintTwoOrMoreNumbers(1, 2):");
PrintTwoOrMoreNumbers(1, 2); // вызов совсем без аргументов невозможен, т.к. первые два параметра mandatoryFirst, mandatorySecond обязательны
Console.WriteLine(">>> Результат вызова PrintTwoOrMoreNumbers(1, 2, 3, 4, 5):");
PrintTwoOrMoreNumbers(1, 2, 3, 4, 5); // 1, 2 - обязательные; 3, 4, 5 - дополнительные опциональные, передадутся в optionalOtherNumbers
Console.WriteLine(">>> Результат вызова PrintTwoOrMoreNumbers(1, 2, new int[] { 5, 6, 7, 8}):");
PrintTwoOrMoreNumbers(1, 2, new int[] { 5, 6, 7, 8 }); // 1, 2 - обязательные; 5, 6, 7, 8 - дополнительные опциональные, передадутся в optionalOtherNumbers
На экране консоли при запуске этой части кода отобразится:
>>> Результат вызова PrintTwoOrMoreNumbers(1, 2):
mandatoryFirst: 1
mandatorySecond: 2
optionalOtherNumbers:
>>> Результат вызова PrintTwoOrMoreNumbers(1, 2, 3, 4, 5):
mandatoryFirst: 1
mandatorySecond: 2
optionalOtherNumbers:
3 4 5
>>> Результат вызова PrintTwoOrMoreNumbers(1, 2, new int[] { 5, 6, 7, 8}):
mandatoryFirst: 1
mandatorySecond: 2
optionalOtherNumbers:
5 6 7 8
Думаю, понятна основная идея этого метода: в обязательных параметрах mandatoryFirst и mandatorySecond мы всегда будем получать значения при вызове метода, а параметр optionalOtherNumbers выступает как опциональный, и его длина может быть равна нулю, либо же там будут какие-то значения, переданные при вызове метода. Вариант с передачей в метод null тут не рассматриваем, т.к. о нём уже поговорили в начале статьи.
Итак, мы рассмотрели с вами различные варианты применения модификатора params для параметров методов в программах на C#. Модификатор params является очень удобным и полезным, когда вам нужно поддержать разное (неопределённое) количество параметров в логике метода или, например, обеспечить для метода вариант его вызова без указания опциональных параметров. Этот модификатор на практике я сам широко использовал и применял при написании многих методов расширения для библиотеки AINStringUtils 2.
Друзья, на этом всё, я надеюсь, что после прочтения статьи у вас не осталось вопросов по использованию модификатора, а если остались - пишите в комментариях или задавайте их в нашей группе в Telegram.
Дополнительно ссылка на документацию: https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/keywords/method-parameters#params-modifier
Ссылка на пример проекта с рассмотренными в статье примерами кода: https://allineed.ru/our-products/download/4-allineed-ru-examples/39-csharp-params-modifier-example