就像这样:
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
无法做到这一点非常令人恼火,最终你只能采用不太优雅的解决方案来解决问题。
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
无法做到这一点非常令人恼火,最终你只能采用不太优雅的解决方案来解决问题。
最近我使用了一个小技巧,取得了很好的效果。我想与那些经常与VBA打交道的人分享。
1.- 在每个自定义类中实现一个公共的初始化子程序。我在所有的类中都称其为InitiateProperties。这个方法必须接受你想要发送到构造函数的参数。
2.- 创建一个名为factory的模块,并创建一个公共函数,函数名称为"Create"+类名,参数与构造函数相同。此函数必须实例化您的类,并调用第(1)点中解释的初始化子程序,传递接收到的参数。最后返回实例化和初始化的方法。
例如:
假设我们有一个自定义类Employee。与前面的示例一样,需要使用姓名和年龄进行实例化。
这是InitiateProperties方法。m_name和m_age是要设置的私有属性。
Public Sub InitiateProperties(name as String, age as Integer)
m_name = name
m_age = age
End Sub
现在在工厂模块中:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Dim employee_obj As Employee
Set employee_obj = new Employee
employee_obj.InitiateProperties name:=name, age:=age
set CreateEmployee = employee_obj
End Function
最后,当您想要实例化一个员工时
Dim this_employee as Employee
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89)
正如 stenci 指出的那样,您可以通过避免在构造函数中创建局部变量来使用更简洁的语法来完成相同的操作。例如,CreateEmployee 函数可以这样编写:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Set CreateEmployee = new Employee
CreateEmployee.InitiateProperties name:=name, age:=age
End Function
哪个更好。
factory.CreateEmployee
以减少歧义... - Peter AlbertSet employee_obj = New Employee
然后 employee_obj.Construct "Johnny", 89
,这样构建过程就在类内部发生了。只是好奇。 - Dick KusleikaClass_Initialize
,然后在那里定义一个变量 m_initialized = false
。当你进入 InitiateProperties
方法时,检查 m_initialized
是否为 false,如果是,则继续并在最后将其设置为 true。如果它为 true,则引发错误或不执行任何操作。如果再次调用 InitiateProperties 方法,它将为 true,对象的状态不会改变。 - bgusach我使用一个包含每个类的一个或多个构造函数的工厂
模块,每个构造函数调用每个类的Init
成员。
例如,一个Point
类:
Class Point
Private X, Y
Sub Init(X, Y)
Me.X = X
Me.Y = Y
End Sub
一个 Line
类Class Line
Private P1, P2
Sub Init(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
If P1 Is Nothing Then
Set Me.P1 = NewPoint(X1, Y1)
Set Me.P2 = NewPoint(X2, Y2)
Else
Set Me.P1 = P1
Set Me.P2 = P2
End If
End Sub
还有一个 Factory
模块:
Module Factory
Function NewPoint(X, Y)
Set NewPoint = New Point
NewPoint.Init X, Y
End Function
Function NewLine(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
Set NewLine = New Line
NewLine.Init P1, P2, X1, Y1, X2, Y2
End Function
Function NewLinePt(P1, P2)
Set NewLinePt = New Line
NewLinePt.Init P1:=P1, P2:=P2
End Function
Function NewLineXY(X1, Y1, X2, Y2)
Set NewLineXY = New Line
NewLineXY.Init X1:=X1, Y1:=Y1, X2:=X2, Y2:=Y2
End Function
这种方法的一个好处是使得在表达式中使用工厂函数变得容易。例如,可以做类似以下的事情:
D = Distance(NewPoint(10, 10), NewPoint(20, 20)
或:
D = NewPoint(10, 10).Distance(NewPoint(20, 20))
这很简洁:工厂仅对所有对象执行非常少的操作,只需创建和调用每个creator的一个Init
函数。
而且它相当面向对象: Init
函数是在对象内部定义的。
编辑
我忘记添加了,这使我能够创建静态方法。例如,我可以这样做(在使参数可选后):
NewLine.DeleteAllLinesShorterThan 10
每次执行时都会创建一个新的对象实例,因此任何静态变量都将丢失。在这个伪静态方法中使用的行集合和任何其他静态变量必须定义在模块中。
当您导出类模块并在记事本中打开文件时,您会注意到在顶部附近有许多隐藏的属性(VBE不显示它们,并且也不公开调整大多数属性的功能)。其中一个是VB_PredeclaredId
:
Attribute VB_PredeclaredId = False
设置为True
,保存并重新导入模块到您的VBA项目中。
PredeclaredId
类具有一个“全局实例”,您可以免费获取 - 就像UserForm
模块一样(导出用户表单,您将看到其predeclaredId属性已设置为true)。
许多人都只是愉快地使用预声明实例来存储状态。 这是错误的 - 它就像在静态类中存储实例状态!
相反,您可以利用默认实例来实现您的工厂方法:
[Employee
类]
'@PredeclaredId
Option Explicit
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As Employee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self 'returns the newly created instance
End With
End Function
Public Property Get Self() As Employee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
有了那个,你可以做到这一点:
Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)
Employee.Create
是在使用 默认实例 进行操作,也就是说它被视为该 类型 的成员,并且只能从默认实例中调用。
问题是,以下情况同样是合法的:
Dim emplFactory As New Employee
Dim empl As Employee
Set empl = emplFactory.Create("Johnny", 69)
这很糟糕,因为现在你有一个令人困惑的API。你可以使用'@Description
注释/ VB_Description
属性来记录用法,但是如果没有Rubberduck,编辑器中没有任何东西可以显示调用站点上的信息。
此外,Property Let
成员是可访问的,因此你的Employee
实例是可变的:
empl.Name = "Jane" ' Johnny no more!
关键是让你的类实现一个接口,只公开需要公开的内容:
[IEmployee
类]
Option Explicit
Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property
现在你需要让 Employee
类 实现 IEmployee
接口 - 最终的类可能会像这样:
[Employee
类]
'@PredeclaredId
Option Explicit
Implements IEmployee
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As IEmployee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self 'returns the newly created instance
End With
End Function
Public Property Get Self() As IEmployee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
Private Property Get IEmployee_Name() As String
IEmployee_Name = Name
End Property
Private Property Get IEmployee_Age() As Integer
IEmployee_Age = Age
End Property
注意现在Create
方法返回的是接口,并且该接口没有暴露Property Let
成员?现在调用代码可以这样写:
Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)
因为客户端编码是针对接口编写的,所以empl
公开的唯一成员是由IEmployee
接口定义的成员,这意味着它看不到Create
方法、Self
访问器或任何Property Let
修改器:因此,代码的其余部分可以使用"抽象"的IEmployee
接口而不是与"具体"的Employee
类一起工作,并享受一个不可变的、多态的对象。
Dim empl as Employee
吗?否则按照你的写法会出现运行时错误。 - PP8Implements IEmployee
,所以 Dim empl As IEmployee
可以正常工作。 - Mathieu GuindonPascalCase
标识符名称。我同意empl
是一个笨拙的前缀,但是关于使用完整类型名称作为后缀的您的观点,我可能会为相关类的名称或接口的不同实现使用此方法,但不适用于变量或属性:empl
将是Employee As IEmployee
,而emplName
将是Name
。话虽如此,您的评论有点难以阅读,但还是谢谢!;-) - Mathieu Guindon使用这个技巧
Attribute VB_PredeclaredId = True
Option Explicit
Option Base 0
Option Compare Binary
Private v_cBox As ComboBox
'
' Class creaor
Public Function New_(ByRef cBox As ComboBox) As ComboBoxExt_c
If Me Is ComboBoxExt_c Then
Set New_ = New ComboBoxExt_c
Call New_.New_(cBox)
Else
Set v_cBox = cBox
End If
End Function
可以看到,New_构造函数被调用来创建和设置类的私有成员(类似于init),唯一的问题是,如果在非静态实例上调用它,它将重新初始化私有成员。但可以通过设置标志来避免这种情况。
首先,这是一个基本的概述/比较基线方法和前三个答案。
基线方法:这些是构建类的新实例的基本方法:
Dim newEmployee as Employee
Dim newLunch as Lunch
'==Very basic==
Set newEmployee = new Employee
newEmployee.Name = "Cam"
newEmployee.Age = 42
'==Use a method==
Set newLunch = new Lunch
newLunch.Construct employeeName:= "Cam" food:="Salad", drink:="Tea"
以上,Construct
将是Lunch类中的一个子程序,它将参数值分配给一个对象。
问题在于,即使有方法,仍需要两行代码,第一行用于设置新对象,第二行用于填充参数。很希望可以在一行中完成两个步骤。
1) 工厂类(bgusach):创建一个单独的类(“工厂”),其中包括创建任何其他所需类的实例以及设置参数的方法。
可能的使用:
Dim f as Factory 'a general Factory object
Dim newEmployee as Employee
Dim newLunch as Lunch
Set f = new Factory
Set newEmployee = f.CreateEmployee("Bob", 25)
Set newLunch = f.CreateLunch("Bob", "Sandwich", "Soda")
当您在代码窗口中键入“f.”时,在Dim f as Factory
之后,您会看到Intellisense可以创建什么的菜单。
2)工厂模块(stenci):与类相同,但工厂可以是标准模块。
可能的用途:
Dim newEmployee as Employee
Dim newLunch as Lunch
Set newEmployee = CreateEmployee("Jan", 31) 'a function
Set newLunch = CreateLunch("Jan", "Pizza", "JuiceBox")
Dim newEmployee as Employee
Dim newLunch as Lunch
Set newEmployee = newEmployee.MakeNew("Ace" 50)
Set newLunch = newLunch.MakeNew("Ace", "Burrito", "Water")
这里的MakeNew
是一个类似于通用工厂类中的CreateEmployee
或CreateLunch
函数,只不过它在要创建的类中,因此我们不必指定它将创建哪个类。
这种第三种方法具有令人着迷的“自我创建”的外观,这是由全局实例允许的。
其他想法:自动实例化、克隆方法或父集合类
通过自动实例化(Dim NewEmployee as new Employee
,注意单词“new”),您可以实现类似于全局实例的功能,而无需进行设置过程:
Dim NewEmployee as new Employee
NewEmployee.Construct("Sam", 21)
在Dim
语句中使用"new",NewEmployee对象会在调用其方法之前被隐式创建。 Construct
是Employee类中的一个子程序,就像基线方法一样。[1]
自动实例化存在一些问题;有些人讨厌它,有些人则为之辩护。2 为了将自动实例化限制为一个原型对象,您可以向类中添加一个MakeNew
函数,就像我在全局实例方法中使用的那样,或者稍微修改一下,改为Clone
:
Dim protoEmployee as new Employee 'with "new", if you like
'Add some new employees to a collection
Dim someNames() as Variant, someAges() as Variant
Dim someEmployees as Collection
someNames = array("Cam", "Bob", "Jan", "Ace")
someAges = array(23, 45, 30, 38)
set someEmployees = new Collection
for i = 0 to 3
someEmployees.Add protoEmployee.Clone(someNames(i), someAges(i))
next
在这里,可以使用可选参数设置Clone
方法Function Clone(optional employeeName, optional employeeAge)
,如果没有提供参数,则使用调用对象的属性。
即使没有自动实例化,在类内部的MakeNew
或Clone
方法可以一行代码创建新对象,一旦创建了原型对象。您还可以以同样的方式将自动实例化用于通用工厂对象,以节省一行代码,或者不使用。
最后,您可能需要一个父类。父类可以具有创建带参数的新子项的方法(例如,使用自定义集合作为Employees,set newEmployee = Employees.AddNew(Tom, 38)
)。对于Excel中的许多对象,这是标准做法:您无法从其父集合中创建工作表或工作簿。
[1]另一个调整与Construct
方法是Sub还是Function有关。如果Construct
从对象中调用以填充其自身属性,则可以是没有返回值的Sub。但是,如果Construct
在填充参数后返回Me
,则前两个答案中的工厂方法/函数可以将参数留给Construct
。例如,使用具有此调整的工厂类可以进行如下操作:Set Sue = Factory.NewEmployee.Construct("Sue", "50")
,其中NewEmployee
是Factory
的一个方法,返回一个空的新员工,但Construct
是Employee
的一个方法,它在内部分配参数并返回Me
。
再增加一种方法。
我提出的解决方案是,如果使用New SomeUserClass
而不是工厂函数,则会引发运行时错误。这样,您的公开成员函数可以在执行其代码之前操作对象已完全初始化的状态。
基本上,您无法将参数传递给类的初始化程序,因此使用如下代码
somevar = New SomeUserClass("Some Initializer")
是行不通的。使用公共创建函数是将初始化程序参数引入其中的一个好方法,但它仍然允许使用somevar = New SomeUserClass
而不受惩罚。在这种情况下,您可以设置一个已初始化标志,并在每个函数中进行检查,但这也很繁琐。
' Factory.bas
Option Explicit
Private m_globalInitializer As Variant
' This could also just be a function
Public Property Get GlobalInitializer() As Variant
If IsEmpty(m_globalInitializer) Then _
Err.Raise 5, , "The factory function must be used to create an instance of this class"
' Error #5 is 'Invalid procedure call or argument'
' Coerce any type of value to be returned
If IsObject(m_globalInitializer) Then
Set GlobalInitializer = m_globalInitializer
Else
GlobalInitializer = m_globalInitializer
End If
' Make this getter a one-time use
m_globalInitializer = vbEmpty
End Property
Public Function CreateSomeUserClass(ByVal somearg1 As String, _
ByVal somearg2 As Object) As SomeUserClass
' You can set m_globalInitializer to anything your class initializer expects
m_globalInitializer = Array(somearg1, somearg2)
' This is the only place this is allowed
Set CreateSomeUserClass = New SomeUserClass
End Function
' SomeUserClass.cls
Option Explicit
Private m_somevar1 As String
Private m_somevar2 As Object
Private Sub Class_Initialize()
Dim args() As Variant
args = GlobalInitializer ' This clears the global initializer as well
' allowing other objects to be factory created
m_somevar1 = args(0)
Set m_somevar2 = args(1)
' Continue rest of initialization, including calling other factories
' that use the GlobalInitializer idiom.
End Sub
另一种方法
假设您创建了一个名为clsBitcoinPublicKey的类
在类模块中创建一个额外的子程序,它的行为就像您希望真正的构造函数一样。下面我将其命名为ConstructorAdjunct。
Public Sub ConstructorAdjunct(ByVal ...)
...
End Sub
From the calling module, you use an additional statement
Dim loPublicKey AS clsBitcoinPublicKey
Set loPublicKey = New clsBitcoinPublicKey
Call loPublicKey.ConstructorAdjunct(...)
Public Sub Init(myArguments)
而不是Private Sub Class_Initialize()
2. 实例化:Dim myInstance As New myClass: myInstance.Init myArguments
RedirectInstance
,需要从私有函数中调用。与类工厂结合使用可以实现不可变性。 - Cristian Buse