Использование делегатов в PowerShell

Что такое делегат (delegate)? Это тип объекта, который при инициализации ассоциируется с неким методом и позволяет нам, обращаясь к делегату, вызывать этот самый метод.

Для чего это нужно? Один из вариантов использования делегатов — это возможность передачи ассоциированного с делегатом метода в качестве параметра другому методу.

PowerShell это поддерживает? Начиная с версии 6.1 — да.

Delegates

Класс TransfromEngine

Решим, что у нас будет класс, содержащий определенный метод для выполнения некоторых действий со строчными значениями, которые будут ему переданы в качестве первого параметра. Вторым же параметром будет определение метода, при помощи которого мы и будем трансформировать полученную строку.

class TransformEngine
{
    static [string] Transform([string] $string, [Func[string, string]] $function)
    {
        return $function.Invoke($string)
    }
}

В первой строке мы задаем имя класса. Пусть это буде TransformEngine.

Далее мы определяем метод Transform. Для упрощения кода пусть он будет статическим.

Возвращать он будет строчное значение, то есть результат трансформации некоторой строки. В качестве первого параметра — $string — метод Transform будет принимать исходную строку, а в качестве второго — $function — он будет получать некий метод, который и будет производить определенную трансформацию. Сигнатуру этого метода мы указываем в определении параметра $function.

Рассмотрим это подробнее.

В качестве типа данных параметра $function у нас указано [Func[string, string]]. Это означает, что в качестве значения данного параметра мы ожидаем некий метод, который возвращает результат, соответствующий последнему типу данных — [Func[string, string]], а в качестве параметров он принимает объекты типов, указанных перед последним типом данных [Func[string, string]], в нашем случае это один параметр того же самого типа, что и результат выполнения указываемого метода.

Будь, к примеру, указано следующее [Func[int, string, bool]] — это бы означало, что передаваемый метод должен принимать два параметра, с типами данных int и string, соответственно, а возвращать он должен объект типа bool.

Далее, в теле метода Transform мы вызываем полученный параметром $function метод, передаем ему значение первого параметра $string и возвращаем результат его выполнения.

Класс Transformers

Теперь давайте создадим класс, содержащий методы, которые мы будем использовать для трансформации строки. Назовем его Transformers.

Вообще, ничто не мешает нам создать нужные нам методы в классе TransformEngine, тем не менее, в целях иллюстрации того, что они могут быть определены где угодно, разнесем эти методы и принимающий их в качестве параметров метод Transform по разным классам.

class Transformers
{
    static [string] TitleCase([string] $string)
    {
        return (Get-Culture).TextInfo.ToTitleCase($string)
    }

    static [string] Reverse([string] $string)
    {
        [string] $reversedString = [string]::Empty

        for ($i = $string.Length - 1; $i -ge 0; $i--)
        {
            $reversedString += $string[$i]
        }

        return $reversedString
    }

    static [string] WordCount([string] $string)
    {
        $wordCount = $string.Split().Count
        return "The string consists of $wordCount words."
    }

    static [string] Highlight([string] $string)
    {
        $firstSpaceIndex = $string.IndexOf(" ")
        return "`e[31m$($string.Substring(0, $firstSpaceIndex))`e[0m$($string.Substring($firstSpaceIndex))"
    }
}

Первый из них, TitleCase, возвращает строку, в которой первая буква каждого слова является заглавной, а все остальные — строчными.

Метод Reverse переворачивает полученную строку задом наперед.

WordCount сообщает о количестве слов в полученной строке.

Метод HighLight использует escape-символы для отображения первого слова красным цветом.

Как вы видите, каждый из этих методов принимает строку в качестве единственного параметра, и ее же возвращает в виде результата.

В качестве строки для преобразования возьмем первое предложение из статьи о кроликах английской версии Википедии.

$string = "Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika)."

Execute

Теперь давайте вызовем метод Transform и передадим ему по очереди каждый из методов класса Transformers.

[TransformEngine]::Transform($string, [Transformers]::TitleCase)
Rabbits Are Small Mammals In The Family Leporidae Of The Order Lagomorpha (Along With The Hare And The Pika).
$engine = [TransformEngine]
$transformers = [Transformers]
$engine::Transform($string, $transformers::Reverse)
.)akip eht dna erah eht htiw gnola( ahpromogaL redro eht fo eadiropeL ylimaf eht ni slammam llams era stibbaR
$wordCount = [Transformers]::WordCount
[TransformEngine]::Transform($string, $wordCount)
The string consists of 19 words.
$highlight = [Transformers]::Highlight
$transform = [TransformEngine]::Transform
$transform.Invoke($string, $highlight)

Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).

Overloads

Теперь давайте предположим, что мы решили выделить функциональность метода Highlight в отдельные классы — HighlightEngine и Highlighters, а также, кроме уже имеющейся возможности выделения первого слова красным цветом, добавить несколько новых, а именно: указание количества слов для выделения и, дополнительно, выбор нужного цвета.

class HighlightEngine
{
    static [string] Highlight([string] $string, [Func[string, string]] $function)
    {
        return $function.Invoke($string)
    }

    static [string] Highlight([string] $string, [int] $numberOfWords, [Func[string, int, string]] $function)
    {
        return $function.Invoke($string, $numberOfWords)
    }

    static [string] Highlight([string] $string, [int] $numberOfWords, [int] $color, [Func[string, int, int, string]] $function)
    {
        return $function.Invoke($string, $numberOfWords, $color)
    }
}

Таким образом, метод Highlight, определенный в этом случае в классе HighlightEngine, будет содержать три оверлоада:

static [string] Highlight([string] $string, [Func[string, string]] $function)

Первый из них, как и раньше, будет принимать исходную строку и метод для ее преобразования.

static [string] Highlight([string] $string, [int] $numberOfWords, [Func[string, int, string]] $function)

Второй, кроме исходной строки и определения метода позволит указывать количество слов для выделения. Как видите, сигнатура принимаемого параметром $function метода тоже отличается.

static [string] Highlight([string] $string, [int] $numberOfWords, [int] $color, [Func[string, int, int, string]] $function)

Третий же будет обладать возможностью указать исходную строку, количество слов, цвет и метод для трансформации строки. В данном случае сигнатура принимаемого метода — [Func[string, int, int, string]] — включает в себя все необходимые для преобразования строки параметры.

Теперь давайте определим методы для трансформации. Расположим их в классе Highlighters.

class Highlighters
{
    static [int] GetIndex([string] $string, [int] $numberOfWords)
    {
        $index = 0
        for ($i = 0; $i -lt $numberOfWords; $i++)
        {
            $index = $string.IndexOf(" ", ++$index)
        }
        return $index
    }

    static [string] Highlighter([string] $string)
    {
        $firstSpaceIndex = $string.IndexOf(" ")
        return "`e[31m$($string.Substring(0, $firstSpaceIndex))`e[0m$($string.Substring($firstSpaceIndex))"
    }

    static [string] Highlighter([string] $string, [int] $numberOfWords)
    {
        $index = [Highlighters]::GetIndex($string, $numberOfWords)
        return "`e[31m$($string.Substring(0, $index))`e[0m$($string.Substring($index))"
    }

    static [string] Highlighter([string] $string, [int] $numberOfWords, [int] $color)
    {
        $index = [Highlighters]::GetIndex($string, $numberOfWords)
        return "`e[${color}m$($string.Substring(0, $index))`e[0m$($string.Substring($index))"
    }
}

Класс Highlighters состоит из служебного метода GetIndex, нужного для получения местоположения символа пробела, следующего за указанным количеством слов, и трех перегрузок метода Highlighter — для каждого из случаев, определенных в классе HighlightEngine.

Теперь, если мы, одну за другой, вызовем все перегрузки метода Highlight, указав в каждом случае одно и то же определение метода Highlighter, мы увидим, что PowerShell автоматически выберет тот оверлоад, чья сигнатура точно соответствует указанной в типе данных параметра $function.

[HighlightEngine]::Highlight($string, [Highlighters]::Highlighter)

Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).

[HighlightEngine]::Highlight($string, 4, [Highlighters]::Highlighter)

Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).

[HighlightEngine]::Highlight($string, 4, 32, [Highlighters]::Highlighter)

Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).

Covariance

Теперь представим, что мы решили выделить в отдельные классы — CalculateEngine и Calculators — механизм вычисления количества слов в полученной строке и, так же, как и в предыдущем случае, расширить доступную функциональность.

Класс CalculateEngine будет содержать метод, в качестве параметров принимающий исходную строку и определение метода, производящего необходимые вычисления.

class CalculateEngine
{
    static [object] Calculate([string] $string, [Func[string, object]] $function)
    {
        return $function.Invoke($string)
    }
}

Класс Calculators будет содержать три метода.

class Calculators
{
    static [string] WordCount([string] $string)
    {
        $wordCount = $string.Split().Count
        return "The string consists of $wordCount words."
    }

    static [object] WordCountInt([string] $string)
    {
        return $string.Split().Count
    }

    static [pscustomobject] WordCountObject([string] $string)
    {
        $wordCount = $string.Split().Count
        return [pscustomobject]@{
            String = $string
            WordCount = $wordCount
            CharacterCount = $string.Length
        }
    }
}

Первый, WordCount, так же, как и раньше, будет выводить строку, сообщающую, из какого количества слов состоит полученная строка.

Метод WordCountInt будет возвращать количество слов в виде числового значения, а именно — int. Для того, чтобы мы смогли возвратить этот тип данных с использованием делегатов, в качестве типа возвращаемого методом WordCountInt значения мы указали [object].

Третий метод — WordCountObject будет возвращать пользовательский объект PowerShell — PSCustomObject, состоящий из трех свойств: исходная строка — String, количество составляющих ее слов — WordCount, а также количество символов — CharacterCount.

Поскольку каждый из этих методов возвращает разный тип данных, и мы собираемся передавать их в качестве значения параметра $function одному и тому же методу Calculate, нам потребуется воспользоваться одним из свойств механизма работы с делегатами, а именно — covariance.

А заключается он в том, что в типе данных параметра, принимающего определения каких-либо методов (signature), в качестве возвращаемого значения мы можем указать тип данных, являющийся базовым по отношению к типам данных, возвращаемым указанными методами (delegates).

Именно поэтому в типе данных параметру $function мы вместо string указали object.

[Func[string, object]] $function

Ну и, для того, чтобы сам метод Calculate мог возвращать различные типы значений, в его определении мы тоже указали object.

static [object] Calculate([string] $string, [Func[string, object]] $function)

Давайте проверим, что у нас получилось.

[CalculateEngine]::Calculate($string, [Calculators]::WordCount)
The string consists of 19 words.
[CalculateEngine]::Calculate($string, [Calculators]::WordCountInt)

19
[CalculateEngine]::Calculate($string, [Calculators]::WordCountObject)
String                                                                                                        WordCount CharacterCount
------                                                                                                        --------- --------------
Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).        19            109

Contravariance

Усложним наш предыдущий пример и решим, что мы хотим иметь возможность получать строчные значения не только напрямую, но и в качестве значений свойств каких-либо объектов, к примеру — экземпляров класса WMI MSFT_NetFirewallRule, и добавим второй оверлоад к методу Calculate.

class CalculateEngine
{
    static [object] Calculate([string] $string, [Func[string, object]] $function)
    {
        return $function.Invoke($string)
    }

    static [object] Calculate([CimInstance] $firewallRule, [Func[CimInstance, object]] $function)
    {
        return $function.Invoke($firewallRule)
    }
}

Этот второй оверлоад вместо строки будет принимать объект CimInstance, являющийся родительским по отношению к MSFT_NetFirewallRule, а в качестве механизма для вычислений — определение метода, принимающего CimInstance и возвращающего object.

Поскольку мы не хотим создавать еще три метода в классе Calculators специально для объектов CimInstance, мы воспользуемся еще одним свойством работы с делегатами — contravariance. Заключается оно в том, что в качестве типов данных параметров теперь уже передаваемых методов (delegates) мы можем указать тип, родительский по отношению к указанным в сигнатуре принимающего их параметра.

class Calculators
{
    static [string] GetString([object] $input)
    {
        if ($input.pstypenames[0] -eq 'Microsoft.Management.Infrastructure.CimInstance#root/standardcimv2/MSFT_NetFirewallRule')
        {
            return $input.Description
        }
        else
        {
            return $input
        }
    }

    static [string] WordCount([object] $input)
    {
        $string = [Calculators]::GetString($input)
        $wordCount = $string.Split().Count
        return "The string consists of $wordCount words."
    }

    static [object] WordCountInt([object] $input)
    {
        $string = [Calculators]::GetString($input)
        return $string.Split().Count
    }

    static [pscustomobject] WordCountObject([object] $input)
    {
        $string = [Calculators]::GetString($input)
        $wordCount = $string.Split().Count
        return [pscustomobject]@{
            String = $string
            WordCount = $wordCount
            CharacterCount = $string.Length
        }
    }
}

Как видите, класс Calculators теперь обладает дополнительным методом — GetString, нужным для получения строки для вычисления из свойства Description объекта MSFT_NetFirewallRule.

Что же касается уже знакомых нам трех методов, то в них, кроме обращения к методу GetString, произошло изменение имени параметра — $string мы заменили на $input — и, что здесь является главным — его типа. Теперь это object, являющийся, как и говорилось выше, родительским по отношению к типам string и CimInstance, указанным в перегрузках метода Calculate.

static [string] WordCount([object] $input)

static [object] WordCountInt([object] $input)

static [pscustomobject] WordCountObject([object] $input)

Перед испытанием измененных методов, давайте зададим переменную $firewallRule, как содержащую объект MSFT_NetFirewallRule, соответствующий правилу межсетевого экрана с именем FPS-ICMP4-ERQ-In.

$firewallRule = Get-NetFirewallRule -Name "FPS-ICMP4-ERQ-In"
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCount)
The string consists of 11 words.
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCountInt)

11
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCountObject)
String                                                          WordCount CharacterCount
------                                                          --------- --------------
Echo Request messages are sent as ping requests to other nodes.        11             63

Return

Как видите, изменения в PowerShell 6.1 делают возможным использование делегатов при работе с классами и методами, что позволяет расширить набор доступных нам инструментов при разработке на PowerShell.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s