Thursday, December 31, 2020

PowerShell: Class Properties Returning Collections (ReadOnlyCollection)

When implementing a class or any code construct, it is critical to avoid side effects. One cause of side effects is when mutable data is provided as a parameter to a method, constructor or function. Additionally side effects can be caused when mutable data is assigned to a property.  To demonstrate host to avoid side effects, consider the following the Friend class:

The first parameter to the constructor defined at line 8 is $name a string type. The string type is immutable. The second parameter is a string array named $tag and it should be recognized that the array type is mutable. Notice at line 10 the following code is invoked making a clone of the $tag string array:

$tag.Clone()

The array created by $tag.Clone() is used to create a value of type of ReadOnlyCollection. The ReadOnlyCollection type cannot be modified hence it is immutable. 

When the GetTags method is invoked (line 17) the value returned therefore cannot be modified by the caller and as the [ReadOnlyCollection[string]] is read-only. The following code demonstrates that the value return by the GetTag method of the Friend class is not subject to side effects:


Line 30 of the code above changes the array passed as a parameter to the constructor of the Friend class. The invoking of $tag.Clone at line means that any change to the array declared at line 26 does not affect $this.tag property of the Friend class. The output from lines 29 and 31 is as follows showing the Friend class instance has not been modified:


Not Invoking the Clone Method

Removing the invocation of $tag.Clone() from line 10 below (see below) makes the instance of the Friend class susceptible to side effects:


The value returned by the GetTags method is of type ReadOnlyCollection but this type is just a read-only wrapper for the underlying string array. Modifying the string array outside of the Friend instance will change the value returned by the GetTags method.

Demonstrating this, the code at line 30 below modifies the tags of the Friend instance, a side effect:


The output from the above code is follows:


The output displayed at line 29 and line 31 differs because line 30 was able to modify the value assigned to the instance of the Friend class by changing the tag "friendly" to "mean".

Not using ReadOnlyCollection

Changing the data type of the $tag property of the Friend class from ReadOnlyCollection to string array also allows side effects. The aforementioned change to the code is shown below and note that line 10 does invoke $tag.Clone():



In the code above, line 6 shows the property $this.tags is now of type [string[]]. The return value of the GetTags method (line 17) is also of type [string[]]. When the code below is invoked, line 31 is able to change the data in the Friend class' $this.tags property:


The output from the code above is as follows:



The changing of the tag from the word, "kind", to the word, "calm" shows that the Friend instance's $this.tags property has been modified. Had the value return by the GetTags method been of type ReadOnlyCollection it would not be possible to modify this return value as there is no method or property exposed by ReadOnlyCollection that allows and instance of this object type to be modified as it is immutable. 

Conclusion

The discussion in this post focused on the properties of classes and the parameters passed to constructors. The same issues with side effects be experienced by the parameters passed to functions or the parameters passed to class methods.

Correct Implementation

# Import-Module '.\PsUnit.psm1'

Set-StrictMode -Version 3.0

class Friend {
    hidden [string] $name 

    hidden 
        [System.Collections.ObjectModel.ReadOnlyCollection[string]] 
                    $tags

    Friend([string] $name, [string[]] $tags) {
        $this.name = $name
        $this.tags =        [System.Collections.ObjectModel.ReadOnlyCollection[
                            string]]::new(
                                $tags.Clone())
    }

    [string] GetName() {
        return $this.name
    }

    [System.Collections.ObjectModel.ReadOnlyCollection[string]] 
                                                    GetTags() {
        return $this.tags
    }

    [string] ToString() {
        return "Name: $($this.name), Tags: $($this.tags -join ':')"
    }
}

[string[]] $tags = @('friendly', 'kind', 'brave')
[Friend] $friend = [Friend]::new('Joe', $tags)

$friend.ToString()
$tags[0] = 'mean'
$friend.ToString()

No comments :

Post a Comment