Reputation: 4263
The following code creates a C# record type, instantiates it using the hashtable object initialization idiom, then modifies the record's field:
Add-Type -TypeDefinition @'
namespace n {
public record r {
public string s {get; init;}
}
}
'@
$r = [n.r]@{s='value'}
$r
$r.s = 'new_value'
$r
The field s
is, ostensibly, read-only after initialization, however, the output is as follows:
s
-
value
new_value
using System;
namespace n {
public record r {
public string s {get; init;}
}
}
public class Program
{
public static void Main()
{
var r = new n.r {s="value"};
r.s = "new_value";
}
}
indeed enforces immutability at compile time with the error
Compilation error (line 13, col 3): Init-only property or indexer 'r.s' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
I am looking for a way to create a type with the following features:
[n.r]@{s='value'}
or similar)Add-Type -ReferenceAssemblies
Can such a type be created?
The code below attempts to accomplish this with each way I could think of to create such an immutable record type in C#. (I'm no C# expert, so I expect I probably missed some.) For reference it also attempts the same with New-Object -Property
. It outputs the following:
OK cl init_error init_origi init_mod_error init_after new_error new_origin new_mod_error new_after_
as nal _mod al mod
s
-- -- ---------- ---------- -------------- ---------- --------- ---------- ------------- ----------
X r1 value new_value value new_value
X r2 value new_value value new_value
X r3 value new_value value new_value
X r4 value new_value value new_value
X r5 Cannot create object of type … The property 's' cannot be fo… The value supplied is not val… The property 's' cannot be fo…
X r6 Cannot convert the "System.Co… The property 's' cannot be fo… A constructor was not found. … The property 's' cannot be fo…
X r7 Cannot convert the "System.Co… The property 's' cannot be fo… A constructor was not found. … The property 's' cannot be fo…
These methods either don't support hashtable object initialization or do allow the object property to be modified after creation.
Add-Type -TypeDefinition @'
namespace n {
public record r1 { public string s {get; init;} }
public record class r2 { public string s {get; init;} }
public record struct r3 { public string s {get; init;} }
public class r4 { public string s {get; init;} }
public class r5 { public string s {get;} }
public class r6 {
public readonly string s;
public r6 (string s) { this.s = s; }
}
public class r7 {
public string s {get;}
public r7 (string s) { this.s = s; }
}
}
'@
$(foreach ($n in 'r1','r2','r3','r4','r5','r6','r7') {
$r = $null
[pscustomobject]@{
class = $n
init_error = $(try { $r = Invoke-Expression "[n.$n]@{s='value'}"}
catch { $_ })
init_original = $r.s
init_mod_error = $(try { $r.s = 'new_value'}
catch { $_ } )
init_after_mod = $r.s
new_error = $(try { $r = New-Object "n.$n" -Property @{s='value'}}
catch {$_})
new_original = $r.s
new_mod_error = $(try { $r.s = 'new_value'}
catch { $_ } )
new_after_mod = $r.s
}
}) |
Select-Object `
-Property @{name='OK';
expression = {if ('value' -in $_.new_after_mod,
$_.init_after_mod ) {'✓'}
else {'X'}}},
* |
Format-Table `
-Property @{e='OK' ; width = 2},
@{e='class' ; width = 2},
@{e='init_error' ; width = 30},
@{e='init_original' ; width = 10},
@{e='init_mod_error' ; width = 30},
@{e='init_after_mod' ; width = 10},
@{e='new_error' ; width = 30},
@{e='new_original' ; width = 10},
@{e='new_mod_error' ; width = 30},
@{e='new_after_mod' ; width = 10}
Upvotes: 1
Views: 88
Reputation: 4263
PowerShell invokes a constructor with argument System.Collections.Hashtable
when that idiom is used. Specifying such a constructor with read-only properties seems to achieve terse named initialization of read-only properties of a plain old C# object.
The code
Add-Type -TypeDefinition @'
namespace n {
public class r {
public string s {get;}
public r(System.Collections.Hashtable h) {
if (h.ContainsKey("s") &&
(null != (h["s"]))) {
this.s = h["s"] as string;
}
}
}
}
'@
$r = [n.r]@{s='value'}
$r
$r.s = 'new_value'
$r
outputs
s
-
value
InvalidOperation:
Line |
18 | $r.s = 'new_value'
| ~~~~~~~~~~~~~~~~~~
| 's' is a ReadOnly property.
value
A more complete such class probably ought to handle other common PowerShell idioms reliably. A constructor that converts from System.Management.Automation.PSObject shown here seems to handle some more of those most common idioms:
public r(System.Management.Automation.PSObject p) {
if (p.Properties["s"]?.Value is System.Management.Automation.PSObject) {
var b = (p.Properties["s"].Value as System.Management.Automation.PSObject).BaseObject;
this.s = b as string;
return;
}
this.s = p.Properties["s"]?.Value as string;
}
The statements
[n.r]$m =
(Get-Date) |
Select-Object -Property @{name='s';expression = {"{0:MMMM}" -f $_}}
$m
for example output
s
-
November
Upvotes: 0