英文:
Efficient method for setting "unknown" property of "unknown" type to "unknown" value?
问题
我正在尝试创建一种自动化方法来设置对象的属性,当属性名称、类型和值都是可变的。
示例:
我有一个 WinForms 窗体 ([System.Windows.Forms.Form]
),我想要设置 Size
、TopMost
和 Dock
属性。我通过 JSON 获取这些值:
"properties": [
{
"name": "Size",
"type": "System.Drawing.Size",
"value": {
"width": 500,
"height": 500
}
},
{
"name": "TopMost",
"type": "System.Boolean",
"value": true
},
{
"name": "Dock",
"type": "System.Windows.Forms.DockStyle",
"value": "Right"
}
]
问题是,由于这些属性都是不同类型的,它们必须以不同的方式设置:
$form.Size = [System.Drawing.Size]::new(500, 500)
$form.TopMost = $true
$form.Dock = [System.Windows.Forms.DockStyle]::Right
目前我正在使用一个 switch
语句:
switch -regex ($property.type){
"^System\.Drawing\.Size$" {
$form.$($property.name) = [System.Drawing.Size]::new($property.value.width, $property.value.height)
}
"^System\.Windows\.Forms\.DockStyle)$" {
$type = [type]$($property.type)
$form.$($property.name) = $type::$($property.value)
}
"^System\.(String|Boolean)$" {
$form.$($property.name) = $property.value
}
Default {
$type = [type]$($property.type)
$form.$($property.name) = $type::new($($property.value))
}
}
我正在使用的 Default
情况对许多类型不起作用,所以每当我遇到一个新类型并希望使用它时,我必须更新 switch
语句。例如,System.Windows.Forms.FlowDirection
是一个像 DockStyle
一样的枚举,因此我必须更新该情况语句:
"^System\.Windows\.Forms\.(DockStyle|FlowDirection))$" {
$type = [type]$($property.type)
$form.$($property.name) = $type::$($property.value)
}
是否有一种统一的方法,无论类型如何都可以工作?
英文:
I'm trying to create an automated method to set the properties of an object, when the property name, type, and value are variable.
Example:
I have a WinForms form ([System.Windows.Forms.Form]
) and I want to set the Size
, TopMost
, and Dock
properties. I'm getting these values via JSON:
"properties": [
{
"name": "Size",
"type": "System.Drawing.Size",
"value": {
"width": 500,
"height": 500
}
},
{
"name": "TopMost",
"type": "System.Boolean",
"value": true
},
{
"name": "Dock",
"type": "System.Windows.Forms.DockStyle",
"value": "Right"
}
]
The problem is, since each of those properties are different types, they all have to be set in different ways:
$form.Size = [System.Drawing.Size]::new(500, 500)
$form.TopMost = $true
$form.Dock = [System.Windows.Forms.DockStyle]::Right
Right now I'm using a switch
statement:
switch -regex ($property.type){
"^System\.Drawing\.Size$" {
$form.$($property.name) = [system.drawing.size]::new($property.value.width, $property.value.height)
}
"^System\.Windows\.Forms\.DockStyle)$" {
$type = [type]$($property.type)
$form.$($property.name) = $type::$($property.value)
}
"^System\.(String|Boolean)$" {
$form.$($property.name) = $property.value
}
Default {
$type = [type]$($property.type)
$form.$($property.name) = $type::new($($property.value))
}
}
The Default
case I'm using doesn't work for a lot of types, so any time I come across a new type that I want to use, I have to update the switch
statement. System.Windows.Forms.FlowDirection
, for example, is an enum like DockStyle
, so I have to update that case statement:
"^System\.Windows\.Forms\.(DockStyle|FlowDirection))$" {
$type = [type]$($property.type)
$form.$($property.name) = $type::$($property.value)
}
Is there a single, unified method that will work, no matter the type?
答案1
得分: 1
根据评论,没有内置的防弹机制来从你的JSON格式反序列化。以下是一个不完整的方法,它对一些常见类型(枚举,值类型,具有符合特定约束的构造函数的类)具有通用处理...
首先,设置一个测试上下文(注意我已经更改了表单大小,以便宽度和高度不同,这样我们可以证明它们被正确设置):
$ErrorActionPreference = "Stop";
Set-StrictMode -Version "Latest";
$properties = @"
{
"properties": [
{
"name": "Size",
"type": "System.Drawing.Size",
"value": {
"width": 1024,
"height": 768
}
},
{
"name": "TopMost",
"type": "System.Boolean",
"value": true
},
{
"name": "Dock",
"type": "System.Windows.Forms.DockStyle",
"value": "Right"
}
]
}
"@ | ConvertFrom-Json;
Add-Type -Assembly "System.Windows.Forms";
$form = new-object System.Windows.Forms.Form;
然后,我们可以使用反射来检查JSON中的每个项目的表单属性。
根据表单属性的类型,我们可以进行一些通用处理,这将在一些限制下工作 - 例如,对于对象属性,类型必须具有与JSON中字段匹配的参数名的公共构造函数。如果对于给定类型这不成立,您仍然需要为该类型添加一个特殊处理程序...
foreach( $property in $properties.properties )
{
write-host $property.name;
# 需要错误处理!
$propertyType = $form.GetType().GetProperty($property.Name).PropertyType;
# 处理简单类型
if( $propertyType -in @( [bool], [string], [int32], [int64] ) )
{
write-host " simple type";
write-host " $($propertyType.FullName)";
write-host " '$($property.value)'";
$form.($property.Name) = $property.value;
continue;
}
# 表单属性是枚举吗?
if( $propertyType.IsEnum )
{
# PowerShell会自动将字符串转换为相应的枚举类型
write-host " enum";
write-host " $($propertyType.FullName)";
write-host " '$($property.value)'";
$form.($property.Name) = $property.value;
continue;
}
# 我们能否使用构造函数创建一个对象?
if( $property.value -is [System.Management.Automation.PSCustomObject] )
{
write-host " constructor";
write-host " $($propertyType.FullName)";
# 我们能否找到类型的适当构造函数?
$valueNames = @( $property.value.psobject.Properties.Name );
$constructors = @(
$propertyType.GetConstructors() `
| where-object {
$params = $_.GetParameters();
# 如果参数数量不同,那么这不是正确的构造函数
if( -not ($params.Length -eq $valueNames.Length ) )
{
return $false;
}
# 是否有构造函数参数
# 在JSON中没有出现?
$missing = $params | where-object {
$valueNames -notcontains $_.Name
}
return ($null -eq $missing)
}
);
if( $constructors.Length -ne 1 )
{
throw "找不到精确匹配的构造函数";
}
write-host " $($constructor.ToString())";
# 注意 - 我们不验证JSON值类型是否与构造函数参数类型兼容
$paramValues = @(
$constructor.GetParameters() | foreach-object {
# 使用“-as”将JSON值转换为构造函数参数的正确类型
write-host " $($_.ParameterType.Name) $($_.Name) = '$($property.value.($_.Name))'";
$property.value.($_.Name) -as $_.ParameterType
};
)
$form.($property.Name) = $constructors[0].Invoke($paramValues);
continue;
}
# 不知道如何处理此类型
throw "未处理的属性类型 '$($propertyType.FullName)'";
}
此操作的日志输出如下:
Size
constructor
System.Drawing.Size
Void .ctor(Int32, Int32)
Int32 width = '1024'
Int32 height = '768'
TopMost
simple type
System.Boolean
'True'
Dock
enum
System.Windows.Forms.DockStyle
'Right'
得到的表单如下:
$form | fl Size, TopMost, Dock
# Size : {Width=1024, Height=768}
# TopMost : True
# Dock : Right
这应该适用于大多数常见的 System.Windows.Forms.Form
属性,但如果您尝试将其用作其他类型的通用反序列化程序,可能会很快失败...
英文:
Per comments, there's no bullet-proof built-in mechanism for deserailising from your json format. Here's an incomplete approach though that has generalised handling for some common types (enums, value types, classes with constructors that meet certain constraints)...
First, set up a test context (note I've changed the form size so the width and height are different so we can prove they're set the right way round):
$ErrorActionPreference = "Stop";
Set-StrictMode -Version "Latest";
$properties = @"
{
"properties": [
{
"name": "Size",
"type": "System.Drawing.Size",
"value": {
"width": 1024,
"height": 768
}
},
{
"name": "TopMost",
"type": "System.Boolean",
"value": true
},
{
"name": "Dock",
"type": "System.Windows.Forms.DockStyle",
"value": "Right"
}
]
}
"@ | ConvertFrom-Json;
Add-Type -Assembly "System.Windows.Forms";
$form = new-object System.Windows.Forms.Form;
and then we can use reflection to inspect the form's properties for each item in the json.
Depending on the type of the form's property we can do some generalised processing that will work with some restrictions - for example for object properties there must be a public constructor for the type with parameter names that match fields in the json. If that's not true for a given type you'll still need to add a special handler for that type...
foreach( $property in $properties.properties )
{
write-host $property.name;
# needs error handling!
$propertyType = $form.GetType().GetProperty($property.Name).PropertyType;
# handle simple types
if( $propertyType -in @( [bool], [string], [int32], [int64] ) )
{
write-host " simple type";
write-host " $($propertyType.FullName)";
write-host " '$($property.value)'";
$form.($property.Name) = $property.value;
continue;
}
# is the form property an enum?
if( $propertyType.IsEnum )
{
# powershell will automatically convert a string to the appropriate enum type
write-host " enum";
write-host " $($propertyType.FullName)";
write-host " '$($property.value)'";
$form.($property.Name) = $property.value;
continue;
}
# can we create an object using a constructor?
if( $property.value -is [System.Management.Automation.PSCustomObject] )
{
write-host " constructor";
write-host " $($propertyType.FullName)";
# can we find an appropriate constructor for the type?
$valueNames = @( $property.value.psobject.Properties.Name );
$constructors = @(
$propertyType.GetConstructors() `
| where-object {
$params = $_.GetParameters();
# not the right constructor if it's got a different number of parameters
if( -not ($params.Length -eq $valueNames.Length ) )
{
return $false;
}
# are there any constructor parameters
# that don't appear in the json?
$missing = $params | where-object {
$valueNames -notcontains $_.Name
}
return ($null -eq $missing)
}
);
if( $constructors.Length -ne 1 )
{
throw "couldn't match to exactly one constructor";
}
write-host " $($constructor.ToString())";
# note - we don't verify json value types are compatible with the constructor parameter types
$paramValues = @(
$constructor.GetParameters() | foreach-object {
# use "-as" to cast the json value to the correct type for the constructor parameter
write-host " $($_.ParameterType.Name) $($_.Name) = '$($property.value.($_.Name))'";
$property.value.($_.Name) -as $_.ParameterType
};
)
$form.($property.Name) = $constructors[0].Invoke($paramValues);
continue;
}
# don't know what to do with this type
throw "unhandled property type '$($propertyType.FullName)'";
}
the logging output from this is:
Size
constructor
System.Drawing.Size
Void .ctor(Int32, Int32)
Int32 width = '1024'
Int32 height = '768'
TopMost
simple type
System.Boolean
'True'
Dock
enum
System.Windows.Forms.DockStyle
'Right'
and the resulting form looks like this:
$form | fl Size, TopMost, Dock
# Size : {Width=1024, Height=768}
# TopMost : True
# Dock : Right
This should work with most of the common System.Windows.Forms.Form
properties, but will probably fail quite quickly if you try to use it as a general-purpose deserialiser for other types...
答案2
得分: 0
可能没有单一的、统一的方法适用于所有类型,但你可以改进你当前的方法:
- 使用
-as
运算符进行类型转换 - 预先解析类型名称,而不是使用正则表达式来识别它
- 为
[System.Boolean]
添加特殊解析
# 解析类型
$type = $property.type -as [type]
# 转换并赋值
$form."$($property.Name)" = switch ($type) {
{$_ -eq [bool]} {
[bool]::Parse($property.value)
}
default {
$property.value -as $type
}
}
英文:
> Is there a single, unified method that will work, no matter the type?
Probably not, but you can improve your current approach:
- Use the
-as
operator for type conversions - Resolve the type name ahead of time instead of using regex to identify it
- Add special parsing for
[System.Boolean]
# resolve the type
$type = $property.type -as [type]
# convert and assign
$form."$($property.Name)" = switch ($type) {
{$_ -eq [bool]} {
[bool]::Parse($property.value)
}
default {
$property.value -as $type
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论