VBA:Sub 的常量参数 - 避免修改按值传递的参数的值

4
我想把一个参数传递给VBA Sub,并确保它不被修改。在C中,我会这样写:void mysub(const int i);。如果有的话,在VBA中实现相同功能的推荐方法是什么(即最简单、最便携等)?
问题涉及下面的第3点。已经有一个被接受的答案,但问题也开放给其他替代方案(可能没有定论)。
编辑:需要根据答案和评论澄清VBA与C之间的功能等价性:
1.引用传递。在VBA中,默认情况下,Sub mysub(i as Integer)(或者Sub mysub(ByRef i as Integer)),使用时不需要从调用者(使用Call mysub(j))那里“请求许可”,可以通过引用来获取参数。在C中,最接近的是void mysub(int * i);。但在C中,还需要使用mysub(&j);这样的调用方式,因此这也取决于调用者是否工作。在被调用的Sub内部,可以修改“变量”(在VBA中为i,在C中为*i)。如果这样做了,它会自动修改调用者中的值。
2.值传递。在VBA中,Sub mysub(ByVal i as Integer)从调用者(使用Call mysub(j))那里获取参数的值(甚至不知道mysub是采用ByVal还是ByRef),对应于C中的void mysub(int i);。在被调用的Sub内部,可以修改“变量”(在VBA或C中为i)。如果这样做了,它不会影响调用者中的值。
3.使用const限定的值传递。在C中,void mysub(const int i);。在被调用的Sub内部,无法修改“变量”(在C中为i)。当然,调用者中的值也不会发生任何变化。
3个回答

5

试试这个:

Sub mysub(ByVal i As Integer)


End Sub

http://msdn.microsoft.com/zh-cn/library/office/aa164263(v=office.10).aspx

重要内容:在定义过程时,您有两种选择来决定如何传递参数:按值传递还是按引用传递。当通过引用传递变量到过程时,VBA实际上传递的是变量在内存中的地址,这使得过程可以直接修改它。当执行返回到调用过程时,变量包含修改后的值。 当通过值传递参数时,VBA将变量的副本传递给过程。然后该过程修改该副本,原始变量的值保持不变;当执行返回到调用过程时,变量包含被传递前相同的值。

编辑:

我现在明白了你想要防止变量在被调用子程序中被修改,而不是在调用子程序中被修改。按照你提出的方式,这是不可能的。VBA只有按值和按引用传递参数的选项。您可以尝试像这样做(这并不完全可靠):

在名为ConstInteger的类模块中:

Private i As Variant
Public Property Get Value() As Integer
    Value = i
End Property
Public Property Let Value(Value As Integer)
    If IsEmpty(i) Then i = Value
End Property

测试:

Sub Caller()
    Dim clsInt As New ConstInteger
    clsInt.Value = 1
    Call Called(clsInt)
End Sub
Sub Called(clsInt As ConstInteger)
    Debug.Print clsInt.Value
    clsInt.Value = 2
    Debug.Print clsInt.Value
End Sub

+1 我猜这就是 OP 实际想要的。 - Siddharth Rout
这不是我要找的。这会防止调用子程序在退出被调用子程序后看到任何修改。我的问题是指如何在被调用子程序内部防止修改。 - sancho.s ReinstateMonicaCellio
@Reafidy - OP提出问题的措辞很好。值参数语义明显不同于const参数。 - Chris Rolliston
1
@Reafidy - 让我们清理评论中的噪音,只留下能传达主题信息的内容。我已经清理了一些。 - sancho.s ReinstateMonicaCellio
@sancho.s - 有时候最好接受不同的编程语言具有不同的特性(例如,C#也没有常量参数)。话虽如此,Blackwater提出的场景(在函数内部有一个想要成为不可变对象的对象)是一个合理的场景,我同意。 - Chris Rolliston
显示剩余22条评论

3
VBA的设计并未考虑如何在函数内部防止参数被修改。如果想要实现该功能,您需要自行实现保护措施。以下是两种可能实现VBA中const保护的建议。
OP的最终解决方案可以在此找到
保护Let和Get方法的参数
以下是一个使用Let和Get属性来防止函数修改类成员变量的示例:
创建一个clsPerson类,并添加以下代码:
Option Explicit

Private m_strName As String
Private m_intAge As Integer
Private m_strAddress As String

Public LockMemberVariables As Boolean

Public Property Get Name() As String
    Name = m_strName
End Property

Public Property Let Name(strName As String)
    If Not LockMemberVariables Then
        m_strName = strName
    Else
        Err.Raise vbObjectError + 1, , "Member variables are locked!"
    End If
End Property

Public Property Get Age() As Integer
    Age = m_intAge
End Property

Public Property Let Age(intAge As Integer)
    If Not LockMemberVariables Then
        m_intAge = intAge
    Else
        Err.Raise vbObjectError + 1, , "Member variables are locked!"
    End If
End Property

Public Property Get Address() As String
    Address = m_strAddress
End Property

Public Property Let Address(strAddress As String)
    If Not LockMemberVariables Then
        m_strAddress = strAddress
    Else
        Err.Raise vbObjectError + 1, , "Member variables are locked!"
    End If
End Property

创建一个普通的代码模块,并包含以下功能:
Option Explicit

Public Sub Main()
    Dim Bob As clsPerson
    Set Bob = New clsPerson

    Bob.Name = "Bob"
    Bob.Age = 30
    Bob.Address = "1234 Anwhere Street"

    Bob.LockMemberVariables = True

    PrintPerson Bob
    AlterPerson Bob

End Sub

Public Sub PrintPerson(p As clsPerson)
    MsgBox "Name: " & p.Name & vbCrLf & _
        "Age: " & p.Age & vbCrLf & _
        "Address: " & p.Address & vbCrLf
End Sub

Public Sub AlterPerson(p As clsPerson)
    p.Name = "Jim"
End Sub

运行Main()函数来测试clsPerson类。这是一种基于状态的运行时方法,可以打开和关闭以防止修改类成员。clsPerson中的每个Property Let都会检查LockMemberVariables是否为true,如果是,则在尝试更改其值时抛出错误。如果您将代码修改为Bob.LockMemberVariables = False,然后调用AlterPerson,则可以无错误地修改Name。
使用接口保护参数
另一种实现保护的方式是通过接口。事实上,您可能更喜欢这种方式,因为它在编译时起作用。假设您创建了一个名为IProtectedPerson的接口(类),该接口仅支持成员变量的Get操作:
Public Property Get Name() As String

End Property

Public Property Get Age() As Integer

End Property

Public Property Get Address() As String

End Property

然后,您需要重写clsPerson类,使其具有常规的Let和Get方法,并且还实现了IProtectedPerson接口:

Option Explicit

Implements IProtectedPerson

Private m_strName As String
Private m_intAge As Integer
Private m_strAddress As String

Public Property Get Name() As String
    Name = m_strName
End Property

Public Property Let Name(strName As String)
    m_strName = strName
End Property

Public Property Get Age() As Integer
    Age = m_intAge
End Property

Public Property Let Age(intAge As Integer)
    m_intAge = intAge
End Property

Public Property Get Address() As String
    Address = m_strAddress
End Property

Public Property Let Address(strAddress As String)
    m_strAddress = strAddress
End Property

'IProtectedPerson Interface Implementation

Private Property Get IProtectedPerson_Name() As String
    IProtectedPerson_Name = Me.Name
End Property

Private Property Get IProtectedPerson_Age() As Integer
    IProtectedPerson_Age = Me.Age
End Property

Private Property Get IProtectedPerson_Address() As String
    IProtectedPerson_Address = Me.Address
End Property

您需要创建以IProtectedPerson为参数的函数。如果您尝试对私有成员变量进行赋值,代码将无法编译,因为该接口上没有Let函数:
Option Explicit

Public Sub Main()
    Dim Bob As clsPerson
    Set Bob = New clsPerson

    Bob.Name = "Bob"
    Bob.Age = 30
    Bob.Address = "1234 Anwhere Street"

    AlterPerson Bob
    AlterProtectedPerson Bob
End Sub

Public Sub AlterPerson(p As clsPerson)
    p.Name = "Jim"
End Sub

Public Sub AlterProtectedPerson(p As IProtectedPerson)
    p.Name = "Sally"
End Sub

代码无法编译,因为在 AlterProtectedPerson 函数中的 p 没有 Name 的 Property Let。

@sancho.s 我会尽可能地为您准备一些好的例子,并在回答中添加它们。现在我看了看您的问题和评论,我认为最好的答案是:“VBA并不是为了防止函数内部参数被修改而设计的。如果您想要这样做,您必须自己实现这些保护措施,作为(可能有些丑陋的)运行时检查。” - Blackhawk
抱歉答复太长了:S - Blackhawk
@Blackhawk - 我打算很快采纳你的答案。我建议:1)为了更好的可读性,让我们删除那些与主题无关的评论。2)我已经添加了一个补充答案,我认为将其超链接对于未来的读者会很有用。 - sancho.s ReinstateMonicaCellio
@sancho.s 我删除了背景信息,只保留了简短的答案和两个示例。我将您的答案链接为“OP的最终解决方案...”。 - Blackhawk

0

这个答案是基于另一个优秀的答案 BH

作为保护参数使用Let和Get (在答案BH中搜索)的替代方法应该如下使用。在一个旨在像called_const(const clsInt As clsPerson)一样工作的函数内(遵循答案BH中的例子), 应该添加

Sub called_const(clsInt As clsPerson)
    clsInt.LockMemberVariables = True
    <Functionality of the Sub>
    clsInt.LockMemberVariables = False
End Sub

然后,如果called_const中的某些代码违反了const规定,它将生成运行时错误(而不是C语言编译错误的情况)。

我可以想到一种(可能不常见的)情况会失败。当从called_const调用另一个类似的Sub(比如called_const2(const clsInt As clsPerson))时。然后,在called_const2结尾执行的解锁操作也将对called_const的其余部分有效。

作为使用接口保护参数在答案BH中搜索)的const替代品似乎完全按照预期工作。

结论

似乎有可能模仿const的行为方式。代价是需要编写额外的代码,并为每个想要使用的const类型定义新的类。

附注:如果const对象的某些成员反过来又是其他对象怎么办?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接