Using delegates in PowerShell

What is a delegate? This is an object type, that instantiates by associating with some method so that we can invoke this method by calling the delegate.

Why is this needed? One of the cases – is that we can use delegate as a parameter for another method.

Is PowerShell supports this? Starting with version 6.1 – yes.

Delegates

TransformEngine class

Let’s create a class, which would have some method for transforming strings, that it will receive as a parameter value. The second parameter of this method will accept method definition, which will actually transform the aforementioned string.

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

The first line defines the class name. Let it be TransformEngine.

Then we define the Transform method. For simplicity let’s make it static.

This method returns a string object – the transformation result of some string. Its first parameter – $string – is a source string, and the second parameter – $function – is the actual method for transforming this string. This method’s signature we specify in the $function parameter declaration.

Let’s look at this more closely.

The $function parameter’s data type is [Func[string, string]]. This means, that as a value we expect some method, which return type is the latter data type of the expression – [Func[string, string]], and it accepts parameters which correspond to all but the latter data type – [Func[string, string]]. In our case both the data types are strings.

If for example $function parameter’s type was [Func[int, string, bool]], that would mean that the parameter accepts methods with parameters of type int and string respectively and return type of bool.

In the Transform method’s body, we invoke the method specified in the $function parameter, pass the string from the $string parameter and return the execution result.

Transformers class

Now, let’s create a class, which will contain the methods we will use for string transformation. We’ll call it Transformers.

Strictly speaking, nothing prevents us from defining needed methods in the TransformEngine class, but in order to showcase, that these methods can come from anywhere, we place them in different class.

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))"
    }
}

The first of the methods – TitleCase returns the string, where the first letter of every word is capital, and all the other are lowercase.

The Reverse method reverses the string.

WordCount reports how many words the string consists of.

And the Highlight method uses escape symbols to display the first word of the string in red.

As you can see, all of the methods accept the string as the only parameter and returns this same type of object.

For the string to transform let’s use the first sentence of the article about rabbits on Wikipedia.

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

Execute

Now let’s call the Transform method and specify every method from the Transformers class as $function parameter value.

[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

Now let’s suppose we intend to separate the highlight functionality into its own classes – HighlightEngine and Highlighters, and besides already existing capability to highlight the first word with red, allow users to specify the number of words to highlight, and, additionally – to choose the color.

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)
    }
}

So, the Highlight method, defined in HighlightEngine class, have three overloads:

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

The first overload, as in the previous example, accepts a source string and a method used for its transformation.

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

The second one, besides the source string and the method definition, accepts the number of words to highlight. As you can see, the signature of the method for the $function parameter is different.

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

The third overload allows to specify the string, the number of words, the color and the transformation method. In this case the delegate’s signature – [Func[string, int, int, string]] – contains even more parameter types, required for string transformation.

Now let’s define the transformation methods. We’ll place them in the Highlighters class.

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))"
    }
}

The Highlighters class consists of the GetIndex method, which is used for getting location of the space symbol, following the specified number of words, and also three overloads of the Highlighter method – one for every use case, as defined in the HighlightEngine class.

Now, if we call every one of the Highlight method overload, and in each case specify the same Highlighter method, we’ll see, that PowerShell automatically choose the overload, which signature corresponds the one, defined in the $function parameter.

[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

Now, let’s suppose, we have decided to transfer word count calculation functionality into separate classes – CalculateEngine and Calculators – and, like in the previous example, to extend its capabilities.

The CalculateEngine class contains the method Calculate, that accepts the source string and the method to perform required calculations.

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

The Calculators class consists of the three methods.

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
        }
    }
}

First method – WordCount, is the same method we saw earlier, and it returns string object, which informs us about the quantity of words the string consist of.

The WordCountInt method returns the word count in a numerical form, specifically – int. In order for us to be able to return the int object using delegates, we specify [object] as a return type of the WordCountInt method.

The third method – WordCountObject – returns the PowerShell custom object – PSCustomObject, which contains three properties: the source string – String, its word count – WordCount, and the character count – CharacterCount.

Because every method returns a different type of objects, and we are going to specify them as the $function parameter of the same Calculate method, we need to take advantage of one of the features delegates possess – covariance.

Covariance means that delegates can be used with methods that have return types that are derived from the return type in the delegate signature.

This is why we specify object as a return type in the $function parameter signature.

[Func[string, object]] $function

Also, in order for the Calculate method to return different types of objects, its return type is specified as object too.

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

Let’s test the methods.

[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

Let’s make this example a little bit more complex and add the ability to use not only the string objects per se, but also to get string from the properties of some objects, say, instances of the MSFT_NetFirewallRule WMI class. In order to do that we are going to add the second overload to the Calculate method.

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)
    }
}

This additional overload instead of string accepts a CimInstance object, which is an ancestor of the MSFT_NetFirewallRule class, and as a calculation method – the one, that accepts CimInstance and returns object.

Because we are not looking forward to creating three new calculation methods specifically for CimInstalnce objects in the Calculators class, we are going to make use of another feature of delegates – contravariance.

This means that delegates can be used with methods that have parameters whose types are base types of the delegate signature parameter type.

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 class now has additional method – GetString, which retrieves the string from the Description property of the MSFT_NetFirewallRule.

Concerning the three existing methods, besides adding a call to the GetString method, there were changes in the parameters name – it is $input now, instead of $string, and – most importantly – their type. Now it is [object], which, as we talked earlier, is an ancestor to string and CimInstance types, specified in the Calculate method overloads.

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

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

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

Before we try this out, let’s define the $firewallRule variable and set its value to the MSFT_NetFirewallRule object, corresponding to FPS-ICMP4-ERQ-In firewall rule.

$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

As you can see, the changes in PowerShell 6.1 allow us to use delegates with classes and methods, which even further expands the number of available tools for developing in PowerShell.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s