替代全局变量或单例

Alternative to Global Variable or Singleton

我在各个地方都读到过,全局变量充其量只是一种代码味道,最好避免使用。目前,我正在努力将基于 PS 脚本的大函数重构为 classes,并考虑使用 Singleton。用例是一个大型数据结构,需要从许多不同的 classes 和模块中引用。 然后我发现 this,这似乎表明单身人士也是个坏主意。

那么,什么是正确的方法(在 PS 5.1 中)来创建需要被很多 classes 引用并被其中一些修改的单一数据结构?可能相关的事实是我不需要它是线程安全的。根据定义,队列将以非常线性的方式处理。

FWIW,我找到了引用的 link 寻找关于单例和继承的信息,因为我的单例只是许多具有非常相似行为的 classes 之一,我从包含下一个 class 的集合的单例,每个包含下一个 class 的集合,以创建一个分层队列。我想要一个基础 class 来处理所有常见的队列管理,然后将其扩展为每个 class 的不同功能。除了让第一个 extended class 成为单身人士之外,这很好用。这似乎是不可能的,对吗?

编辑:或者,是否可以使用通用列表 属性 方法中的嵌套 classes 来识别子项中的父项?这就是我处理这个基于函数的版本的方式。全局 [XML] 变量构成了数据结构,我可以单步执行该结构,使用 .SelectNode() 填充变量以传递给下一个函数,并使用 .Parent 从中获取信息更高,尤其是从数据结构的根部开始。

编辑:由于我现在似乎无法在此处粘贴代码,因此我在 GitHub 上有一些代码。 Singleton 出现的示例位于第 121 行,我需要验证是否还有尚未执行的同一任务的任何其他示例,因此我可以跳过除最后一个实例之外的所有示例。这是删除各种 Autodesk 软件的通用组件的概念证明,这些组件以非常特别的方式进行管理。所以我希望能够安装任何程序(包)的组合并按任何计划卸载,并确保最后一个具有共享组件卸载的包是卸载它的包。为了在最后一次卸载发生之前不破坏其他依赖程序。希望这是有道理的。 Autodesk 的安装过程充满了痛苦。如果你不必和他们打交道,那你就觉得自己很幸运。 :)

如评论中所述,您链接到的代码中没有任何内容需要单例。

如果您想保留 ProcessQueue 和相关 Task 实例之间的父子关系,可以从结构上解决。

只需要在 Task 构造函数中注入一个 ProcessQueue 实例:

class ProcessQueue
{
  hidden [System.Collections.Generic.List[object]]$Queue = [System.Collections.Generic.List[object]]::New()
}

class Task
{
  [ProcessQueue]$Parent
  [string]$Id
  Task([string]$id, [ProcessQueue]$parent)
  {
    $this.Parent = $parent
    $this.Id = $id
  }
}

实例化对象层次结构时:

$myQueue = [ProcessQueue]::new()
$myQueue.Add([Task]@{ Id = "id"; Parent = $myQueue})

... 或重构 ProcessQueue.Add() 以负责构建任务:

class ProcessQueue
{
  [Task] Add([string]$Id){
    $newTask = [Task]::new($Id,$this)
    $Queue.Add($newTask)
    return $newTask
  }
}

此时您只需使用 ProcessQueue.Add() 作为 [Task] 构造函数的代理:

$newTask = $myQueue.Add($id)
$newTask.DisplayName = "Display name goes here"

下次您需要从单个 Task 实例中搜索相关任务时,您只需执行以下操作:

$relatedTasks = $task.Parent.Find($whatever)

为了补充 - 这可能是您问题的最佳解决方案 - 回答您原来的问题:

So, what IS the right way (in PS 5.1) to create a single data structure that needs to be referenced by a lot of classes, and modified by some of them [without concern for thread safety]?

  • 要避免全局变量的主要原因是它们是会话 -global,意味着在之后执行你自己的代码也会看到这些变量,这可能会产生副作用。

  • 您不能在 PowerShell 中实现真正的单例,因为 PowerShell classes 不支持 访问修饰符;值得注意的是,你不能使构造函数私有(非public),你只能使用hidden关键字"hide"它,这只会使它不易被发现,同时仍然可访问

  • 您可以使用以下技术近似一个单例,它本身模拟一个static class(PowerShell 也不支持,因为 static 关键字仅在 class members 上受支持,而不是class整体)。

一个简单的例子:

# NOT thread-safe
class AlmostAStaticClass {
  hidden AlmostAStaticClass() { Throw "Instantiation not supported; use only static members." }
  static [string] $Message    # static property
  static [string] DoSomething() { return ([AlmostAStaticClass]::Message + '!') }
}

[AlmostAStaticClass]::<member>(例如,[AlmostAStaticClass]::Message = 'hi')现在可以在定义 AlmostAStaticClass 的范围内使用 和所有后代范围 (但它 全局可用,除非定义范围恰好是全局范围。

如果您需要跨模块边界class访问,您可以将其作为参数传递(作为输入文字);请注意,您仍然需要 :: 来访问(总是静态的)成员;例如,
& { param($staticClass) $staticClass::DoSomething() } ([AlmostAStaticClass])


实现一个线程安全准单例 - 或许可以使用 使用 ForEach-Object -Parallel (v7+) or Start-ThreadJob(v6+,但可安装在 v5.1 上)- 需要 更多工作

注:

    然后需要
  • 方法 来获取和设置概念上的属性,因为 PowerShell 不支持代码支持的 属性 getter 和 setter自 7.0 起(添加此能力是 this GitHub feature request 的主题)。

  • 但是你仍然需要一个底层的属性,因为PowerShell不支持字段;同样,你能做的最好的事情就是 隐藏 这个 属性,但它在技术上仍然可以访问。

以下 示例使用 System.Collections.Concurrent 命名空间中的 System.Threading.Monitor (which C#'s lock statement is based on) to manage thread-safe access to a value; for managing concurrent adding and removing items from collections, use the thread-safe collection types

# Thread-safe
class AlmostAStaticClass {

  static hidden [string] $_message = ''  # conceptually, a *field*
  static hidden [object] $_syncObj = [object]::new() # sync object for [Threading.Monitor]

  hidden AlmostAStaticClass() { Throw "Instantiation not supported; use only static members." }

  static SetMessage([string] $text) {
    Write-Verbose -vb $text
    # Guard against concurrent access by multiple threads.
    [Threading.Monitor]::Enter([AlmostAStaticClass]::_syncObj)
    [AlmostAStaticClass]::_message = $text
    [Threading.Monitor]::Exit([AlmostAStaticClass]::_syncObj)
  }

  static [string] GetMessage() {
    # Guard against concurrent access by multiple threads.
    # NOTE: This only works with [string] values and instances of *value types*
    #       or returning an *element from a collection* that is 
    #       only subject to concurrency in terms of *adding and removing*
    #       elements.
    #       For all other (reference) types - entire (non-concurrent) 
    #       collections or individual objects whose properties are
    #       themselves subject to concurrent access, the *calling* code 
    #       must perform the locking.
    [Threading.Monitor]::Enter([AlmostAStaticClass]::_syncObj)
    $msg = [AlmostAStaticClass]::_message
    [Threading.Monitor]::Exit([AlmostAStaticClass]::_syncObj)
    return $msg
  }

  static [string] DoSomething() { return ([AlmostAStaticClass]::GetMessage() + '!') }

}

请注意,与跨越模块边界类似,使用线程也需要将 class 作为类型对象传递给其他线程,但是使用 $using: 范围说明符更方便;一个简单的(人为的)例子:

# !! BROKEN AS OF v7.0
$class = [AlmostAStaticClass]
1..10 | ForEach-Object -Parallel { ($using:class)::SetMessage($_) }

注意:由于目前 classes,这种跨线程使用实际上 从 v7.0 开始被破坏绑定到 定义 运行空间 - 参见 this GitHub issue。看会不会给出解决方案


如您所见,PowerShell classes 的局限性使得实现此类场景变得很麻烦;将 Add-Type 与临时编译的 C# 代码一起使用是值得考虑的替代方案。

GitHub meta issue是与PowerShell相关的各种问题的汇编classes;虽然它们最终可能会得到解决,但 PowerShell 的 classes 不太可能达到与 C# 相同的功能;毕竟,OOP 不是 PowerShell 脚本语言的重点(使用 预先存在的对象除外)。