Создано при помощи нейросети Kandinsky

Модификатор params для параметров методов в C#

Разработка на C# Просмотров: 73

User Rating: 0 / 5

Изображение для статьи создано при помощи нейросети Kandinsky

Доброго времени суток, друзья.

В этой статье мы рассмотрим с вами на примерах применение модификатора params в объявлениях методов в программе на C#.

Этот модификатор бывает полезен, когда вам необходимо вызывать метод с различным количеством аргументов.

При указании модификатора 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