【问题标题】:How to efficiently convert object values into an array?如何有效地将对象值转换为数组?
【发布时间】:2020-08-08 11:03:09
【问题描述】:

我通过 Powershell 将一个非常大的 CSV 表导入到对象数组中。 每个对象可能看起来像这样的定义:

$test = [PsCustomObject]@{id='1';name='a'}

问题是,所有类型都是“字符串”,我需要稍后在我的代码中使用正确的类型。为此,我想使用嵌入式 C# 代码将所有对象属性的值转换为字符串数组,以将其值添加到具有正确类型定义的 DataTable 中。

我现在在 C# 部分苦苦挣扎。这是我的代码示例,它不起作用。 如何更改 C# 部分以将对象值转换为数组?

# this code should convert the object-values into an array:
Add-Type -ReferencedAssemblies System.Data -TypeDefinition '
using System;
public class row {
    public object[] row(object test) {
        int id = Int32.Parse(test.id);
        string name = test.name;
        return (id, name);
    }
}'

# this is the test-object:
$test = [PsCustomObject]@{id='1';name='a'}

# this is the final table with correct types:
$table = [System.Data.DataTable]::new()
[void]$table.Columns.Add('id', [int])
[void]$table.Columns.Add('name', [string])

# at the end it should work like this:
$row = [row]::new($test)
$table.Rows.Add($row)

我在没有 C# 的情况下做了一些测试,但这很慢。 例如。这个简单的循环(即使没有将数据添加到行中)运行超过 20 秒:

$test = [PsCustomObject]@{id='1';name='a'}
foreach($x in 1..1MB) {
    $row = foreach($i in $test.PsObject.Properties.Value){if ($i) {$i} else {$null}}
    #[void]$table.rows.Add($row)
}

所以理论上我需要在最后一个代码块中做同样的事情,但通过嵌入式 Csharp 代码。

我怎样才能以有效的方式完成这项工作?

更新 #1: 感谢 Theo 的输入,我可以加快转换速度。我没想到这会比仅查询 PsObject-properties 快 5 倍。例如。事实证明,“else”语句比首先分配 var 要慢。下面是对比代码:

$test = [PsCustomObject]@{id='1';name='a'}
foreach($x in 1..1MB) {
    $id = $test.id
    if ([string]::IsNullOrEmpty($id)){$id = $null}
    $name = $test.name
    $row = @($id, $name)
}

但它仍然是我整个代码中最慢的部分,我仍在寻找智能 C# 解决方案。我的想法是,如果稍后输入对象有任何其他属性,那么我可以动态重建 C# 代码。这不适用于纯 PS 代码。

更新 #2: 根据 BACON 的输入,我能够使用 C# 代码解决挑战。 这是我的工作实现:

Add-Type -TypeDefinition '
using System;
public class test {
    public string id {get;set;}
    public string name {get;set;}
}
public static class DataParser {
    public static string[] ParseToArray(test data) {
        string id = data.id;
        if (String.IsNullOrEmpty(id)) {id = null;};
        string name = data.name;
        return new string[] {id,name};
    }
}'

# this is the test-object:
$test = [PsCustomObject]@{id='';name='a'}

$timer = [System.Diagnostics.Stopwatch]::StartNew()
foreach($x in 1..1MB) {
    $row = [DataParser]::ParseToArray($test)
}
$timer.Stop()
$timer.Elapsed.TotalSeconds

我没想到的是这个解决方案的运行时间——它比我上面发布的纯 PowerShell 版本慢得多。所以我的结论是“任务完成”,但我们错过了目标。这意味着没有真正有效的方法将对象值转换为数组。

因此,我将不再将 CSV 数据作为对象导入,而是专注于通过“dataSet.readXML”将大型数据作为 XML 导入。我只希望有一个内置选项可以直接将 CSV 数据导入为数组或 dataRows。

【问题讨论】:

  • 并非如此。我需要将几十万个上述格式的对象插入到数据表中。这就是为什么我正在寻找通过 CSharp 实现从属性值转换为可用于创建数据行的数组的原因。
  • 我如何才能获得与我遇到的真正挑战相关的问题的负面评价?
  • 我猜,你的问题(最初)被否决了,就像XY Problem 一样,从你的结论中也可以看出。为什么要将其转换为 DataTable?是什么让您认为您的 C# 类将胜过本机 PowerShell?如果您通过 PowerShell 管道正确地流式传输对象,您可以节省大量内存,它可能会更快,但您不会显示任何“慢”的东西。顺便说一句,实际问题源于不支持字符串以外的任何其他类型的 csv 文件(不是 PowerShell)。您可以简单地使用计算表达式:
  • .. | Select-Object @{n='ID'; e={ [Int]$_.ID }}, Name | ...

标签: c# performance powershell csv datatable


【解决方案1】:

纯 PowerShell 解决方案是:

[int]$refInt = 0 # create an int as reference variable for TryParse()
foreach($item in $test) {
    # get the int value or $null for the id property
    $rowId = if ([int]::TryParse($item.id, [ref]$refInt)) { $refInt } else { $null }
    # get the string value or $null for the name property
    $rowName = $item.name.ToString()    # added ToString() for good measure
    if ([string]::IsNullOrWhiteSpace($rowName)) { $rowName = $null }
    # add a new row to the table
    $newRow = $table.NewRow()
    $newRow["id"]   = $rowId
    $newRow["Name"] = $rowName
    $null = $table.Rows.Add($newRow)
}

我不是很喜欢 C#,但我认为您还需要在那里使用 TryParse() 以获得 int 或 $null 值。至于name 属性,您还应该检查它的 NullOrWhiteSpace 并在其上使用ToString() 方法以确保您获得有效的字符串或 $null。

【讨论】:

  • 如果TryParse() 失败,它会将第二个参数设置为0,所以如果它超出了有效ID 的范围并且不需要$null,具体来说,表示“失败parse" 然后[int] $rowId = -1; [void] [int]::TryParse($item.id, [ref] $rowId) 会快一点。问题中的代码没有设置它创建的DataColumns 的AllowDBNull property,尽管它默认为$true
  • 有什么办法可以动态地创建用于此转换的 powershell 代码,以跳过读取对象的 PSObject.properties 并在代码后面以硬编码方式使用属性的非常缓慢的实现?跨度>
【解决方案2】:

你没有说你的基于 C# 的尝试是如何“不工作”的,但我可以看到一些问题......

using System;
public class row {
    public object[] row(object test) {
        int id = Int32.Parse(test.id);
        string name = test.name;
        return (id, name);
    }
}

这定义了一个名为row 的类,其中一个实例method(不是constructor)也名为row。您没有定义任何构造函数,因此row 类将只有一个默认的无参数构造函数。当你这样做时......

$row = [row]::new($test)

...您正在尝试调用不存在的 row 构造函数重载。

此外,row() 的返回类型是 object[],但 (id, name)(value) tuple,而不是 array。编译需要从前者到后者的一些转换。

调用您的Add-Type 命令提醒我...

Add-Type: (4,21): error CS0542: 'row': 成员名称不能与其封闭类型相同
   公共对象 [] 行(对象测试){
                   ^

...这说明了自己,并且...

Add-Type: (5,35): 错误 CS1061: 'object' 不包含 'id' 的定义,并且找不到可访问的扩展方法 'id' 接受类型为 'object' 的第一个参数(是您缺少 using 指令或程序集引用?)

...这意味着由于 test 参数的 compile-time 类型是 object,除非您将其转换为更具体的类型,否则您将只能访问object class 的成员,它没有属性或字段。由于testrun-time 类型将是PSCustomObject——这是一种“神奇”类型——动​​态访问id 和带有reflectionname 属性将不起作用。

所以,基本上,问题在于,尽管有一些表面上的相似之处,C# 是 very different 而不是 PowerShell 并且不能这样写。您可以通过将值填充到中来解决上述问题一种更友好的 C# 访问类型,像这样...

using System;
public static class DataParser {
    public static object[] ParseToArray(Tuple<string, string> data) {
        int id = int.Parse(data.Item1);
        string name = data.Item2;
        return new object[] { id, name };
    }
}

请注意,Tuple&lt;,&gt; generic type 与之前链接的元组不同;那个需要 C# 7.0,所以,为了更好的兼容性,我在这里不使用它。然后你可以像这样调用上面的方法......

$testTuple = [Tuple]::Create($test.id, $test.name)
$testAsArray = [DataParser]::ParseToArray($testTuple)
$table.Rows.Add($testAsArray)

更简单的方法是消除中间对象并仅通过参数传递属性...

using System;
public static class DataParser {
    public static object[] ParseToArray(string id, string name) {
        return new object[] { int.Parse(id), name };
    }
}

...然后这样称呼它...

$testAsArray = [DataParser]::ParseToArray($test.id, $test.name)
$table.Rows.Add($testAsArray)

鉴于这两种方法的实现除了将其输入填充到一个数组之外所做的更多,下一个也是最好的优化是认识到 C# 代码没有做足够的工作来证明其使用的合理性 并将其完全删除。因此,我们只是直接在 PowerShell 中创建数组...

$testAsArray = [Int32]::Parse($test.id), $test.name
$table.Rows.Add($testAsArray)

现在,这简化了您的代码,但它并没有实现使其更快的目标。正如我所说,您需要在 C# 方法中做更多的工作——比如接受所有输入记录,适当地解析它们,并填充 DataTable——以使其有价值。为此,我认为您需要展示更多代码;具体来说,如何从 CSV 文本到内存记录,以及每条记录是否真的存储为 PSCustomObject(由 Import-Csv 返回)或其他内容。

【讨论】:

  • 感谢您的回答。这非常有帮助,并为我提供了很多见解。不幸的是,动态创建 C# 代码以确保 PS 部分不包含任何硬编码属性,如“id”和“name”,这对我的想法不起作用。最后,我想使用该 c# sn-p 将任何类型的对象值转换为数组。尤其是当对象具有超过 8 个属性时,如果不嵌套元组就无法使用它们。
  • 除非你想让它解析任何看起来像 [Int32][Int32] 等的东西,否则你将不得不在某个地方对属性名称或索引进行硬编码 所以代码,无论是 C# 还是 PowerShell,都知道哪些属性需要转换为哪些类型。 Here's a recent PowerShell answer of mine 实现了这两种方法。在没有看到更多代码的情况下,我无法提供任何进一步的信息,但就您在已删除答案中提出的问题而言,我建议您重新阅读该答案的最后两段。
  • 感谢您的帮助。如果这有意义与否,请查看我的更新#2。有什么明显的原因导致速度如此缓慢?
猜你喜欢
  • 2021-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-14
  • 2017-09-23
  • 2019-12-02
相关资源
最近更新 更多