【问题标题】:Powershell arguments by reference: implied vs explicitPowershell 引用参数:隐含与显式
【发布时间】:2021-10-02 02:47:21
【问题描述】:

Powershell 以两种不同的方式通过引用支持参数。任何变量都可以使用[Ref] 作为类型通过引用传递,或者基本上我猜想转换为特殊的[Ref] 类型。或者,像[Hashtable] 这样的复杂数据类型总是通过引用传递。前一种方法需要在更改值或将变量传递给另一个类方法或函数时使用.value。简而言之,这就是实践中的样子。

# implicit By Reference
class One_i {
    static [Void] Act ([System.Collections.Generic.List[string]] $data) {
        $data.Add("One_i::Act(): $(Get-Date)")
        [Two_i]::Act($data)
    }
}

class Two_i {
    static [Void] Act ([System.Collections.Generic.List[string]] $data) {
        $data.Add("Two_i::Act(): $(Get-Date)")
    }
}

# explicit By Reference
class One_e {
    static [Void] Act ([Ref] $data) {
        $data.value.Add("One_e::Act(): $(Get-Date)")
        [Two_e]::Act($data.value)
    }
}

class Two_e {
    static [Void] Act ([Ref] $data) {
        $data.value.Add("Two_e::Act(): $(Get-Date)")
    }
}



CLS
$data_i = [System.Collections.Generic.List[string]]::new()
$data_i.Add("Init_i: $(Get-Date)")
[One_i]::Act($data_i)
$data_i.Add("Finalize_i: $(Get-Date)")
foreach ($item in $data_i) {
    Write-Host "$item"
}
Write-Host

$data_e = [System.Collections.Generic.List[string]]::new()
$data_e.Add("Init_e: $(Get-Date)")
[One_e]::Act($data_e)
$data_e.Add("Finalize_e: $(Get-Date)")
foreach ($item in $data_e) {
    Write-Host "$item"
}
Write-Host

我对我更喜欢哪种方法感到矛盾。一方面[Ref] 明确了哪些变量是By Ref,这很有用。然而,对.value 的需求使代码更难阅读,增加了工作量,并创造了忘记和引发错误的机会。我是这样测试性能的...

(measure-Command {
    $data_i = [System.Collections.Generic.List[string]]::new()
    $data_i.Add("Init_i: $(Get-Date)")
    foreach ($i in 1..1000) {
        [One_i]::Act($data_i)
    }
    $data_i.Add("Finalize_i: $(Get-Date)")
}).TotalSeconds

(measure-Command {
    $data_e = [System.Collections.Generic.List[string]]::new()
    $data_e.Add("Init_e: $(Get-Date)")
    foreach ($e in 1..1000) {
        [One_e]::Act($data_e)
    }
    $data_e.Add("Finalize_e: $(Get-Date)")
}).TotalSeconds

并且隐式似乎要快得多,但不足以仅根据性能来为隐式进行论证。当我试图决定采用哪种方法时,这导致我提出了许多问题,或者可能意识到每种方法在某些情况下都是更好的选择,并且在单个程序中使用这两种方法实际上是有好处的。

1:为什么复杂类型与简单类型的处理方式不同?不一致的行为对我来说似乎很奇怪,我怀疑如果我了解行为不同的原因,我会学到一些基础知识。

2:除了是否需要使用.value之外,隐式和显式引用之间还有其他区别吗?尤其是其中一种方法是否存在任何潜在问题?

3:是否有强制复杂类型按值的机制?那会有什么用例?

4:我找到了this thread,有一条评论说最好避免引用(但没有解释)。但是,我还发现对依赖注入的引用优于全局变量和/或单例模式,并且按引用是 DI 的基础。是否有烦人的重要事项我需要在这里注意,特别是因为它与 PowerShell/类/依赖注入有关,因为 PowerShell 类没有像其他语言那样完全实现。我在 PS 5.1 中工作并且不会很快迁移到 Core,但很高兴知道 Core 和前 Core 实现之间是否存在根本差异。

编辑:基于@mclayton 的回答和我对“改变价值”与“突变”的理解,我尝试“打破”这样的参考

# implicit By Reference
class One_i {
    static [Void] Act ([System.Collections.Generic.List[string]] $data) {
        $data.Add("One_i::Act(): $(Get-Date)")
        [Two_i]::Act($data)
        $data = [System.Collections.Generic.List[string]]::new()
    }
}
# explicit By Reference
class One_e {
    static [Void] Act ([Ref] $data) {
        $data.value.Add("One_e::Act(): $(Get-Date)")
        [Two_e]::Act($data.value)
        $data.value = [System.Collections.Generic.List[string]]::new()
    }
}

我在“显式”示例中尝试了$data.value = [System.Collections.Generic.List[string]]::new()$data = [System.Collections.Generic.List[string]]::new(),但实际上似乎没有一个主题会导致问题。在所有三种情况下,控制台输出都是“正确的”。这表明我根本不明白改变价值和突变之间的区别。

也就是说,由于它与我提出问题的最初原因有关,即决定我要使用两种方法中的哪一种,我倾向于我所说的“隐式”,因为它确实使代码更简单一些通过消除对.value 的需要,并且变量的引用性质不明显并不会改变我必须理解它是引用的事实,无论我做什么。我真的希望我能找到一个讨论这两种方法的相对优点的线程,但到目前为止,我的搜索未能找到任何有用的东西。

Edit2:所以,我发现this mklement0 说这两种方法在功能上确实没有区别。我突然想到,我可以通过使用[Ref] 然后将$ref.value 分配给局部变量来获得两全其美的效果,以简化方法或函数中的事情。暂时的感觉是最好的答案。它在每个函数或方法的顶部添加了一行,但使调用在行为上更加明显,并简化了在方法或函数体中的使用。

# explicit By Reference with sugar
class One_x {
    static [Void] Act ([Ref] $data) {
        $localData = $data.Value
        $localData.Add("One_x::Act(): $(Get-Date)")
        [Two_e]::Act(([Ref]$localData))
    }
}

class Two_x {
    static [Void] Act ([Ref] $data) {
        $localData = $data.Value
        $localData.Add("Two_x::Act(): $(Get-Date)")
    }
}

【问题讨论】:

    标签: powershell pass-by-reference pass-by-value


    【解决方案1】:

    tl;dr

    以下是值类型、引用类型、值传递和引用传递之间的主要区别:

    Pass by value Pass by reference
    Value type
    • Assignments to parameters inside a function are not persisted to outer variables
    • Assignments to parameters inside a function are persisted to outer variables
    Reference type
    • Assignments to parameters inside a function are not persisted to outer variables
    • Mutations to object inside function are visible outside function
    • Assignments to parameters inside a function are persisted to outer variables
    • Mutations to object inside function are visible outside function

    Loooooooong 版本

    首先,我认为[ref] 的文档有点误导,尽管过去对其进行了改进(请参阅about_RefImprove the about_Ref topic to clarify its primary purpose)。

    主要问题是“值类型与引用类型”和“按值传递与按引用传递”之间存在细微差别 - about_Ref 文档涉及它,但有点将其模糊为一个大的单一概念.

    值类型与引用类型

    暂时忽略[ref],.Net 世界中有两种基本类型,它们作为参数传递的方式有一些相似之处

    值类型的变量包含该类型的一个实例。这与引用类型的变量不同,后者包含对类型实例的引用。默认情况下,在赋值、将参数传递给方法并返回方法结果时,会复制变量值。对于值类型变量,复制相应的类型实例。

    使用引用类型,两个变量可以引用同一个对象;因此,对一个变量的操作会影响另一个变量引用的对象。

    值类型和引用类型的默认行为是“按值”传递——也就是说,传递给方法的参数是父作用域中变量值的副本。对于值类型,这意味着整个值的完整副本(包括结构类型),对于引用类型,这意味着将重复的 reference 传递给相同的底层对象。为方法内的参数分配新值或对象引用不会超出方法的范围,因为它只影响参数,它是外部值类型或引用的 copy -类型变量。

    使用about_Ref中的示例:

    在下面的例子中,函数改变了传递给它的变量的值。在 PowerShell 中,整数是值类型,因此它们是按值传递的。因此,$var 的值在函数范围之外是不变的。

    (注意 - 这可能应该说“函数改变了传递给它的参数的值”,因为它实际上并没有改变外部的值变量)

    Function Test($data)
    {
        $data = 3
    }
    
    $var = 10
    Test -data $var
    $var
    # 3 # no change
    

    $var 是值类型时,在函数内部为$data 分配一个新值对函数外部$var 的值没有影响,因为函数传递了一个值的“副本”-类型变量,任何更改只会影响副本。

    显示$var 变量的引用类型的类似示例在函数完成后也未更改:

    Function Test($data)
    {
        $data = @{ "Test" = "New Text" }
    }
    
    $var = @{ "xxx" = "yyy"}
    Test -data $var
    $var
    # @{ "xxx" = "yyy" } # no change
    

    同样,$data 是对与 $var 变量中相同的引用类型对象的重复 引用 - 即使它们指向同一个对象,它们也是独立的引用,因此分配$data 的新值不会影响 $var

    但是请注意,about_Ref 也给出了这个例子:

    在以下示例中,将包含 Hashtable 的变量传递给函数。 Hashtable 是一种对象类型,因此默认情况下它通过引用传递给函数。

    通过引用传递变量时,函数可以更改数据,并且该更改在函数执行后仍然存在。

    这有点误导,因为:

    • 哈希表不是“对象类型”,而是“引用类型”
    • 参数不是“通过引用传递给函数”——它实际上是一个引用类型的变量,通过值传递给函数!
    Function Test($data)
    {
        $data.Test = "New Text"
    }
    
    $var = @{}
    Test -data $var
    $var
    # @{ "Test" = "New Test" }
    

    为了澄清,这个例子并没有改变$data参数的(即它指向哪个对象)——它只是在参数的引用类型对象上调用一个实例方法指向,并且该方法变异对象。由于外部作用域中的 $var 变量指向相同的引用类型对象,因此函数中对 $data 的更改对函数外部的 $var 是“可见的”,因为它们都引用了相同的对象记忆。

    在PowerShell解析了合适的类型加速器之后基本上就相当于这个了:

    Function Test($data)
    {
        $data.Add("Test", "New Text")
    }
    
    $var = @{}
    Test -data $var
    $var
    # @{ "Test" = "New Text" }
    

    这就是值类型和引用类型之间的重要区别——我们可以在函数内部改变引用类型,并且当函数存在时,外部变量中的更改将是可见的,因为即使参数和变量是独立的引用,它们引用同一个对象,所以通过函数参数进行的突变在外部变量中是可见的。

    总结默认情况下,为函数内部的参数赋值不会影响在父作用域中赋值给变量的内容,无论是否父变量是值类型或引用类型。引用类型对象可以在函数内通过调用参数引用的对象上的方法变异,并且效果在函数外的变量中可见,因为它们是指向同一个引用类型的对象。

    按值传递与按引用传递

    当您按值传递变量时,您会得到上述“值类型与引用类型”中所述的行为 - 即,对函数内部参数的赋值不会持续返回到父范围内的变量中。

    相比之下,当您通过reference传递变量时,您可以为父作用域中的变量分配新的值类型或引用类型值:

    通过引用传递使函数成员、方法、属性、索引器、运算符和构造函数能够更改参数的值,并使该更改在调用环境中持续存在。

    about_Ref 说:

    无论传递的数据类型如何,您都可以对函数进行编码以将参数作为参考。

    虽然它可能应该说“您可以编写函数以通过 引用传递参数”以避免与“引用类型参数”混淆。

    它给出的例子是一个通过引用传递的值类型参数:

    Function Test([ref]$data)
    {
        $data.Value = 3
    }
    
    $var = 10
    Test -data ([ref]$var)
    $var
    # 3
    

    请注意,在此函数调用后,Test 内部对$data.Value 的更改会持续到$var,因为变量 是通过引用传递的。

    对于通过引用传递的引用类型也是如此:

    Function Test([ref]$data)
    {
        $data.Value = @{ "Test" = "New Text" }
    }
    
    $var = @{ "xxx" = "yyy" }
    Test -data ([ref]$var)
    $var
    # @{ "Test" = "New Text" } # new value
    

    在这种情况下,我们没有改变原始哈希表 - 我们只是将一个完全不同的哈希表分配给 $var。我们可以用几行代码来证明这一点:

    $new = @{ "xxx" = "yyy" }
    $old = $new
    Test -data ([ref]$new)
    
    $new
    # @{ "Test" = "New Text" } # new value
    
    $old
    # @{ "xxx" = "yyy" } # old value is still there unchanged
    

    $new$old 在函数调用之前都指向同一个哈希表,但之后$new 指向完全不同的对象。

    总结:当通过引用传递参数时,给函数内部的参数赋新值也会给父作用域内的变量赋新值,而不管父变量是否为值类型或引用类型。

    总结

    回到您最初的问题,您现在可以希望看到您的两个示例并不真正等效:

    class One_i {
        static [Void] Act ([System.Collections.Generic.List[string]] $data) {
            $data.Add("One_i::Act(): $(Get-Date)")
            [Two_i]::Act($data)
        }
    }
    
    class One_e {
        static [Void] Act ([Ref] $data) {
            $data.value.Add("One_e::Act(): $(Get-Date)")
            [Two_e]::Act($data.value)
        }
    }
    

    没有“隐式”和“显式”[ref] 这样的 - 两种方法都只是在原地改变 $data 引用类型参数的内容,但(如果它愿意)One_e 可能会改变$data_e 引用的对象,在函数调用结束后将被持久化到 $var 中。为了证明这一点,请尝试在两个方法的末尾添加 $data.Value = @{ "xxx" = "yyy" } 并查看哪个版本将更改保留回 $data_i$data_e(您可能会猜到 One_e 会:-)。

    【讨论】:

    • @mklement0 - 为@ 道歉,但如果有人可以帮助纠正或加强我的回答,我知道是你 :-)。如果它无法挽救,也请随意说,我会删除它!
    • 看来我的问题的一部分是词汇之一。在我看来,“更改值”意味着更改变量中包含的数据,在这种情况下,我真的应该考虑“变异”。虽然从突变的角度来看,这两种方法在功能上是相同的,但[Ref] 开辟了重新定义变量的可能性,这将是一个错误,所以我所说的“隐式”可能更安全,因为我只想更改包含的数据在变量中。这是否更接近准确的理解(以及合理的决定)?还是我仍然误解了价值?
    • 所以,从我对原始问题的编辑中可以看出,我似乎不明白其中的细微差别。我还想在这里补充一点,如果有人能深入了解 PowerShell 开发团队的想法,我希望看到答案,这就是为什么数组和哈希表默认为按引用,而简单的字符串和整数却没有的原因。这让我感觉很奇怪,但我确信有一个真正正当的理由让我现在无法理解。
    • @mclayton 您的回答中缺少一个方面是运行时 (.NET) 区分值类型(结构和整数类型,包括枚举值)和引用类型(“复杂类型”),而这对我们在 PowerShell 中传递参数的方式有根本性的影响
    • @Gordon - 我稍后会尝试更新此答案,但您可能想阅读以下内容:leerichardson.com/2007/01/parameter-passing-in-c.html。它可能有助于理解“值类型与引用类型”和“按值传递与按引用传递”这两个独立的概念。 [Ref] 功能适用于当您想要“通过引用传递”时 - 它基本上类似于 C# 中的 out 关键字。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-20
    • 1970-01-01
    • 2011-09-15
    • 1970-01-01
    • 1970-01-01
    • 2019-09-28
    相关资源
    最近更新 更多