有效的方法来设置“未知”类型的“未知”属性为“未知”值?

huangapple go评论73阅读模式
英文:

Efficient method for setting "unknown" property of "unknown" type to "unknown" value?

问题

我正在尝试创建一种自动化方法来设置对象的属性,当属性名称、类型和值都是可变的。

示例:

我有一个 WinForms 窗体 ([System.Windows.Forms.Form]),我想要设置 SizeTopMostDock 属性。我通过 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
}
}

huangapple
  • 本文由 发表于 2023年6月1日 07:47:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76377899.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定