问题描述

这个宏是为特定数量的 Civ V 玩家随机抽取特定数量的文明选择。它基于 43 种可能性的列表,while 循环中的 for 循环检查已选择的文明。恒定单元格引用指向工作表中使用 scroll-bars 获得的值。使用基于变量的单元格引用来构造 two-column 设计中的结果。

有没有什么聪明的方式使我的代码从美学和功能的角度更好?

Sub CIV_draw()
Range("K2:M50").ClearContents
Dim x As Integer
x = Cells(3, 3).Value
For i = 1 To x
    Cells(3 + (Cells(3, 7).Value + 2) * (i - 1), 11).Value = "Player " & i
    For j = 1 To Cells(3, 7).Value
        Dim Rand_CIV As String
        Dim RandNum As Integer
        Dim checkVal As Boolean
        checkVal = False
        While checkVal = False
            RandNum = CInt(Int(43 * Rnd())) + 1
            Rand_CIV = Cells(RandNum, 16).Value & " (" & Cells(RandNum, 15).Value & ")"
            For Z = 1 To 50
                If Cells(Z, 12) = Rand_CIV Then
                    Exit For
                End If
                If Z = 50 Then
                    checkVal = True
                End If
            Next Z
        Wend

        Cells((Cells(3, 7).Value + 2) * (i - 1) + (j + 3), 12).Value = Rand_CIV
    Next j
Next i
End Sub

我知道例如,我的重复检查不是最佳的,我想获得替代解决方案的建议。

最佳解决方法

那么,Rubberduck 只表示一个问题。

Rubberduck Code Inspections – 3/8/2015 12:56:11 PM 1 issue found. Suggestion: Member ‘CIV_draw’ is implicitly Public – VBAProject.Module1, line 3

Implicit Public Member documentation

所以,你是一个好的开始。您可以通过显式声明子例程的范围来纠正这一点。

Public Sub CIV_draw()

就我个人而言,我也会把这个前缀加在一起,只需调用这个例程 Draw()就可以了。下划线在 VBA 中占有特殊地位。它通常表示事件过程或接口实现,所以我将避免在其他地方的过程名称中使用下划线。


接下来我注意到,您正在调用 Range()Cells()方法,而不指定工作表。这意味着您在 ActiveSheet 上隐式调用这些方法。应该避免这种情况,因为在执行代码时,用户将很少改变工作表。您应该创建一个工作表变量,该变量将在最初设置,然后使用它。

Dim ws As Worksheet
Set ws = ThisWorkbook.ActiveSheet

ws.Range("K2:M50").ClearContents
' ...

你的代码中还有很多魔术数字,但是我会回头看看。首先,我们来谈谈提取一些很有名的方法。随机数逻辑是一个很好的解决方案。当读这个方法时,我们并不在乎如何产生随机数,只是我们得到一个。

RandNum = CInt(Int(43 * Rnd())) + 1 
Private Function GetRandomNum() As Integer
    GetRandomNum = CInt(Int(43 * Rnd())) + 1
End Function

接下来是获得随机文明字符串的业务。

Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String
    GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")"
End Function

这已经使您的代码更高级的可读性。如果有人想要细节,他们可以挖掘这些私人功能。我们也通过这样做完全消除了一个变量。

Dim RandCivilization As String
Dim checkVal As Boolean

checkVal = False
While checkVal = False
    RandCivilization = GetRandomCivilization(GetRandomNum(), ws)
    For Z = 1 To 50

接下来我们来清理你的 While 循环。

Dim checkVal As Boolean  checkVal = False While checkVal = False 

接下来,让我们翻转逻辑来使用 Not 条件。

Dim checkVal As Boolean
checkVal = False

While Not checkVal

最后,给它一个更好的名字。

Dim endOfRange As Boolean
endOfRange = False

While Not endOfRange

下一个业务顺序是将这个重复的逻辑提取到自己的方法中。

(ws.Cells(3, 7).Value + 2) 
Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)
    GetNumberOfCivilizations = ws.Cells(3, 7).Value
End Function

这使得代码的当前状态为此。

Public Sub Draw()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.ActiveSheet

    ws.Range("K2:M50").ClearContents

    Dim x As Integer
    x = ws.Cells(3, 3).Value

    Dim numberOfCivs As Integer
    numberOfCivs = GetNumberOfCivilizations()

    Dim i As Long
    For i = 1 To x
        ws.Cells(3 + (numberOfCivs + 2) * (i - 1), 11).Value = "Player " & i

        Dim j As Long
        For j = 1 To numberOfCivs
            Dim RandCivilization As String
            Dim endOfRange As Boolean

            endOfRange = False
            While Not endOfRange
                RandCivilization = GetRandomCivilization(GetRandomNum(), ws)

                Dim z As Long
                For z = 1 To 50
                    If ws.Cells(z, 12) = RandCivilization Then
                        Exit For
                    End If
                    If z = 50 Then
                        endOfRange = True
                    End If
                Next z
            Wend

            ws.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 12).Value = RandCivilization
        Next j
    Next i
End Sub

Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)
    GetNumberOfCivilizations = ws.Cells(3, 7).Value
End Function

Private Function GetRandomNum() As Integer
    GetRandomNum = CInt(Int(43 * Rnd())) + 1
End Function

Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String
    GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")"
End Function

这是一个改进,但我们仍然在处理很多数字,对我们来说并不意味着我们,直到我们回到实际的工作表。我们为它们定义一些常量值,以及一些负责从工作表获取数据的函数。

Private Function GetNumberOfPlayers(ByVal ws As Worksheet) As Integer
    GetNumberOfPlayers = ws.Cells(3, 3).Value
End Function

还有另一个功能来获取我们写出结果的范围。

Private Function GetResultsRange(ByVal ws As Worksheet) As Range
    Set GetResultsRange = ws.Range("K2:M50")
End Function

现在,请注意,我们正在引用结果范围内的列位置,而不是相对于工作表的位置。一个人在只看代码的时候更容易理解。

Public Sub Draw()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.ActiveSheet

    Dim resultsRange As Range
    Set resultsRange = GetResultsRange(ws)
    resultsRange.ClearContents

    Dim numOfPlayers As Integer
    numberOfPlayers = GetNumberOfPlayers()

    Dim numberOfCivs As Integer
    numberOfCivs = GetNumberOfCivilizations()

    Dim i As Long
    For i = 1 To numberOfPlayers
        resultsRange.Cells(3 + (numberOfCivs + 2) * (i - 1), 1).Value = "Player " & i

        Dim j As Long
        For j = 1 To numberOfCivs
            Dim RandCivilization As String
            Dim endOfRange As Boolean

            endOfRange = False
            While Not endOfRange
                RandCivilization = GetRandomCivilization(GetRandomNum(), ws)

                Dim z As Long
                For z = 1 To resultsRange.Rows.Count
                    If resultsRange.Cells(z, 2) = RandCivilization Then
                        Exit For
                    End If
                    If z = resultsRange.Rows.Count Then
                        endOfRange = True
                    End If
                Next z
            Wend

            resultsRange.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 2).Value = RandCivilization
        Next j
    Next i
End Sub

在这一点上,你应该得到这个想法。继续提取良好命名的函数,直到您的主程序中没有重复或模糊的逻辑。这个代码段将是我的下一个目标。

(numberOfCivs + 2) * (i - 1) 

以及一些负责将数据写入结果范围的子站。


当我停止审查时,我的 IDE 中的代码:

Option Explicit  Public Sub Draw()     Dim ws As Worksheet     Set ws = ThisWorkbook.ActiveSheet      Dim resultsRange As Range     Set resultsRange = GetResultsRange(ws)     resultsRange.ClearContents      Dim numOfPlayers As Integer     numberOfPlayers = GetNumberOfPlayers()      Dim numberOfCivs As Integer     numberOfCivs = GetNumberOfCivilizations()      Dim i As Long     For i = 1 To numberOfPlayers         resultsRange.Cells(3 + (numberOfCivs + 2) * (i - 1), 1).Value = "Player " & i          Dim j As Long         For j = 1 To numberOfCivs             Dim RandCivilization As String             Dim endOfRange As Boolean              endOfRange = False             While Not endOfRange                 RandCivilization = GetRandomCivilization(GetRandomNum(), ws)                  Dim z As Long                 For z = 1 To resultsRange.Rows.Count                     If resultsRange.Cells(z, 2) = RandCivilization Then                         Exit For                     End If                     If z = resultsRange.Rows.Count Then                         endOfRange = True                     End If                 Next z             Wend              resultsRange.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 2).Value = RandCivilization         Next j     Next i End Sub  Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)     GetNumberOfCivilizations = ws.Cells(3, 7).Value End Function  Private Function GetRandomNum() As Integer     GetRandomNum = CInt(Int(43 * Rnd())) + 1 End Function  Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String     GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")" End Function  Private Function GetNumberOfPlayers(ByVal ws As Worksheet) As Integer     GetNumberOfPlayers = ws.Cells(3, 3).Value End Function  Private Function GetResultsRange(ByVal ws As Worksheet) As Range     Set GetResultsRange = ws.Range("K2:M50") End Function 

次佳解决方法

$O:$P 是应用程序数据,它们不完全属于您的 front-end 工作表。

我将这两列复制到另一个工作表,插入标题行,添加标题 CivilizationNameLeaderName,然后将该范围转换为表。是什么赋予了?桌子是最有用的 Excel 的强大功能,也可以使用它们!

在 VBA 方面的事情,它大大简化了访问数据 – 您可以给工作表一个编程名称,所以我将工作表命名为表 Civilizations,并将表本身命名为 tblCivilizations(“tbl” 前缀有用于区分 Excel 公式中不同类型的命名范围) 。

这意味着 VBA 现在可以访问这个表:

Dim civilizationsTable As ListObject
Set civilizationsTable = Civilizations.ListObjects(1)

然后可以使用 civilizationsTable.ListRows 迭代行,这意味着您可以在 i 行的第 2 列中获取值,如下所示:

civilizationsTable.ListRows(i).Range(ColumnIndex:=2)

这意味着这样一条线:

Rand_CIV = Cells(RandNum, 16).Value & " (" & Cells(RandNum, 15).Value & ")"

可以这样写:

civName = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=1)
civLeader = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=2)
civCaption = civLeader & " (" & civName & ")"

更多的代码,你会说?等等,我还没完成

Public Enum CivilizationTableColumns
    CivilizationName = 1
    CivilizationLeader '= 2
End Enum

把它变成这样:

civName = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=CivilizationName)
civLeader = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=CivilizationLeader)
civCaption = civLeader & " (" & civName & ")"

然后我们可以买到一个 ListRow 对象:

Set row = civilizationsTable.ListRows(RandNum)
civName = row.Range(ColumnIndex:=CivilizatioName)
civLeader = row.Range(ColumnIndex:=CivilizationLeader)
civCaption = civLeader & " (" & civName & ")"

魔数什么魔数? … 然后那个小块可以被提取到自己的小功能中:

Private Function GetCivilizationCaption(ByVal index As Long)
    Set row = civilizationsTable.ListRows(index)
    civName = row.Range(ColumnIndex:=CivilizatioName)
    civLeader = row.Range(ColumnIndex:=CivilizationLeader)
    GetCivilizationCaption = civLeader & " (" & civName & ")"
End Function

这里的外卖是因为缺乏抽象,很难看清楚发生了什么。我们的小人类大脑喜欢抽象。证明 – 你宁愿保持什么,6 个月下来?

Cells((Cells(3, 7).Value + 2) * (i - 1) + (j + 3), 12).Value = Rand_CIV

要么..

AssignCivilizationForPlayer player, civilizationCaption

参考文献

注:本文内容整合自 Google/Baidu/Bing 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。