Als een groot voorstander van versiebeheer in Microsoft Access, moet ik het hebben over mijn grootste probleem met de VBA-ontwikkelomgeving:het automatisch "herschikken" van identifiers. Zie dit als een uitbreiding van mijn antwoord op een vraag over deze "functie" op stackoverflow.
Ik ga dit artikel in twee delen benaderen. In deel 1 zal ik het gedrag van de ontwikkelomgeving definiëren. In deel 2 zal ik mijn theorie bespreken over waarom het zo werkt.
Deel 1:Het gedrag definiëren
Als je enige tijd hebt besteed aan het schrijven van code in VBA, weet ik zeker dat je deze "functie" hebt opgemerkt. Terwijl u id's typt - variabelen, functienamen, opsommingen, enz. - merkt u misschien dat de IDE automatisch de hoofdletters van deze id's verandert. U kunt bijvoorbeeld een variabelenaam in kleine letters typen, maar zodra u naar een nieuwe regel gaat, verandert de eerste letter van uw variabele plotseling in hoofdletters.
De eerste keer dat je dit ziet, kan het schokkend zijn. Terwijl je doorgaat met programmeren, blijft de IDE de zaak schijnbaar willekeurig veranderen. Maar als je genoeg tijd in de IDE doorbrengt, wordt het patroon uiteindelijk vanzelf zichtbaar.
Om te voorkomen dat u meer dan tien jaar van uw leven moet wachten tot het patroon zich aan u openbaart, zal ik nu het patroon beschrijven zoals ik het ben gaan begrijpen. Voor zover ik weet, heeft Microsoft dit gedrag nooit officieel gedocumenteerd.
- Alle automatische hoofdletterwijzigingen zijn globaal voor het VBA-project.
- Telkens wanneer de declaratieregel van een van de volgende typen id's wordt gewijzigd, wordt ook het hoofdlettergebruik van elke andere id met dezelfde naam gewijzigd:
- Subnaam
- Functienaam
- Typ naam
- Enum naam
- Variabelenaam
- Constante naam
- Naam eigendom
- Telkens wanneer de naam van een enum-item ergens in de code wordt gewijzigd, wordt het hoofdlettergebruik van de naam van het enum-item bijgewerkt zodat het overal overeenkomt.
Laten we nu wat gedetailleerder over elk van deze gedragingen praten.
Globale veranderingen
Zoals ik hierboven schreef, zijn wijzigingen in identifier-cases globaal voor een VBA-project. Met andere woorden, de VBA IDE negeert het bereik volledig bij het wijzigen van het geval van identifiers.
Stel dat u bijvoorbeeld een privéfunctie heeft met de naam AccountIsActive in een standaardmodule. Stel je nu een klassenmodule voor ergens anders in datzelfde project. De klassenmodule heeft een eigen Property Get-procedure. Binnen die Property Get-procedure bevindt zich een lokale variabele met de naam accountIsActive . Zodra u de regel Dim accountIsActive As Boolean
. typt in de VBA IDE en ga naar een nieuwe regel, de functie AccountIsActive die we afzonderlijk in zijn eigen standaardmodule hebben gedefinieerd, is de declaratieregel gewijzigd in Private Function accountIsActive()
om overeen te komen met de lokale variabele binnen deze klassenmodule.
Dat is een hele mondvol, dus laat me het beter in code demonstreren.
Stap 1:Definieer de functie AccountIsActive
'--== Module1 ==--
Private Function AccountIsActive() As Boolean
End Function
Stap 2:Declareer accountIsActive lokale variabele in een ander bereik
'--== Class1 ==--
Private Sub Foo()
Dim accountIsACTIVE As Boolean
End Sub
Stap 3:VBA IDE...wat heb je gedaan?!?!
'--== Module1 ==--
Private Function accountIsACTIVE() As Boolean
End Function
VBA Case-Obliteration's non-discriminatiebeleid
VBA is niet tevreden met het simpelweg negeren van de reikwijdte, maar negeert ook verschillen tussen soorten identifiers in zijn zoektocht om behuizingconsistentie op te leggen. Met andere woorden, elke keer dat u een nieuwe functie, subroutine of variabele declareert die een bestaande identifier-naam gebruikt, worden alle andere instanties van die identifier gewijzigd om overeen te komen.
In elk van deze onderstaande voorbeelden is het enige dat ik verander de eerste module die wordt vermeld. De VBA IDE is verantwoordelijk voor alle andere wijzigingen aan eerder gedefinieerde modules.
Stap 1:Definieer een functie
'--== Module1 ==--
Public Function ReloadDBData() As Boolean
End Function
Stap 2:Definieer een sub met dezelfde naam
OPMERKING:Dit is perfect geldig zolang de procedures zich in verschillende modules bevinden. Dat gezegd hebbende, alleen omdat je *kan* iets doen, wil nog niet zeggen dat je *moet*. En je *moet* deze situatie vermijden als dat enigszins mogelijk is.
'--== Module2 ==--
Public Sub ReloadDbData()
End Sub
'--== Module1 ==--
Public Function ReloadDbData() As Boolean
End Sub
Stap 3:Definieer een type met dezelfde naam
OPMERKING:Nogmaals, definieer geen sub, functie en typ niet allemaal dezelfde naam in één project.
'--== Module3 ==--
Private Type ReLoadDBData
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub ReLoadDBData()
End Sub
'--== Module1 ==--
Public Function ReLoadDBData() As Boolean
End Sub
Stap 4:Definieer een opsomming met dezelfde naam
OPMERKING:Alsjeblieft, alsjeblieft, alsjeblieft, uit liefde voor alle heilige dingen...
'--== Module4 ==--
Public Enum ReloadDbDATA
Dummy
End Enum
'--== Module3 ==--
Private Type ReloadDbDATA
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub ReloadDbDATA()
End Sub
'--== Module1 ==--
Public Function ReloadDbDATA() As Boolean
End Sub
Stap 5:Definieer een variabele met dezelfde naam
OPMERKING:doen we dit eigenlijk nog steeds?
'--== Module5 ==--
Public reloaddbdata As Boolean
'--== Module4 ==--
Public Enum reloaddbdata
Dummy
End Enum
'--== Module3 ==--
Private Type reloaddbdata
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub reloaddbdata()
End Sub
'--== Module1 ==--
Public Function reloaddbdata() As Boolean
End Sub
Stap 6:Definieer een constante met dezelfde naam
OPMERKING:Oh, kom op. Serieus?
'--== Module6 ==--
Private Const RELOADDBDATA As Boolean = True
'--== Module5 ==--
Public RELOADDBDATA As Boolean
'--== Module4 ==--
Public Enum RELOADDBDATA
Dummy
End Enum
'--== Module3 ==--
Private Type RELOADDBDATA
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub RELOADDBDATA()
End Sub
'--== Module1 ==--
Public Function RELOADDBDATA() As Boolean
End Sub
Stap 7:Definieer een klasse-eigenschap met dezelfde naam
OPMERKING:dit wordt gek.
'--== Class1 ==--
Private Property Get reloadDBData() As Boolean
End Property
'--== Module6 ==--
Private Const reloadDBData As Boolean = True
'--== Module5 ==--
Public reloadDBData As Boolean
'--== Module4 ==--
Public Enum reloadDBData
Dummy
End Enum
'--== Module3 ==--
Private Type reloadDBData
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub reloadDBData()
End Sub
'--== Module1 ==--
Public Function reloadDBData() As Boolean
End Sub
Enum-items?!?!
Voor dit derde punt is het belangrijk om onderscheid te maken tussen een Enum-type en een Enum item .
Enum EnumTypeName ' <-- Enum type
EnumItemAlice ' <-- Enum item
EnumItemBob ' <-- Enum item
End Enum
We hebben hierboven al laten zien dat Enum-typen op dezelfde manier worden behandeld als andere soorten declaraties, zoals subs, functies, constanten en variabelen. Telkens wanneer de aangifteregel voor een identifier met die naam wordt gewijzigd, wordt het hoofdlettergebruik van elke andere identifier in het project met dezelfde naam geüpdatet om overeen te komen met de laatste wijziging.
Enum items zijn speciaal omdat ze de enige soort identifier zijn waarvan de behuizing kan worden gewijzigd wanneer elke regel code die de naam van het enum-item bevat, wordt gewijzigd.
Stap 1. Definieer en vul de Enum
'--== Module7 ==--
Public Enum EnumTypeName
EnumItemAlice
EnumItemBob
End Enum
Stap 2. Raadpleeg de Enum-items in code
'--== Module8 ==--
Sub TestEnum()
Debug.Print EnumItemALICE, EnumItemBOB
End Sub
Resultaat:Enum-type declaratie verandert om overeen te komen met de reguliere coderegel
'--== Module7 ==--
Public Enum EnumTypeName
EnumItemALICE
EnumItemBOB
End Enum
Deel 2:Hoe zijn we hier gekomen?
Ik heb nog nooit met iemand van het interne VBA-ontwikkelteam gesproken. Ik heb nog nooit officiële documentatie gezien over waarom de VBA IDE werkt zoals het werkt. Dus wat ik ga schrijven is pure gissing, maar ik denk dat het logisch is.
Lange tijd heb ik me afgevraagd waarom de VBA IDE dit gedrag zou vertonen. Het is tenslotte duidelijk de bedoeling. Het gemakkelijkste voor de IDE om te doen zou zijn ... niets. Als de gebruiker een variabele in hoofdletters declareert, laat deze dan in hoofdletters staan. Als de gebruiker een paar regels later naar die variabele in kleine letters verwijst, laat die verwijzing dan in kleine letters en de originele declaratie in hoofdletters.
Dit zou een perfect acceptabele implementatie van de VBA-taal zijn. De taal zelf is immers niet hoofdlettergevoelig. Dus waarom zou je al die moeite doen om de hoofdletters van de ID automatisch te veranderen?
Ironisch genoeg geloof ik dat de motivatie was om verwarring te voorkomen. (Swing and a miss, als je het mij vraagt.) Ik spot met deze uitleg, maar het is wel logisch.
Contrast met hoofdlettergevoelige talen
Laten we het eerst hebben over programmeurs die uit een hoofdlettergevoelige taal komen. Een gebruikelijke conventie in hoofdlettergevoelige talen, zoals C#, is om klasseobjecten met hoofdletters te noemen en om instanties van die objecten dezelfde naam te geven als de klasse, maar met een voorloop kleine letter.
Die conventie werkt niet in VBA, omdat twee identifiers die alleen in hoofdletters verschillen, als equivalent worden beschouwd. In feite laat de Office VBA IDE je niet tegelijkertijd een functie declareren met één type behuizing en een lokale variabele met een ander soort behuizing (we hebben dit hierboven uitvoerig besproken). Dit voorkomt dat de ontwikkelaar aanneemt dat er een semantisch verschil is tussen twee identifiers met dezelfde letters maar met een ander hoofdletter.
Onjuiste code er verkeerd uit laten zien
De waarschijnlijker verklaring in mijn gedachten is dat deze "functie" bestaat om equivalente identifiers er identiek uit te laten zien. Denk er over na; zonder deze functie zouden typefouten gemakkelijk kunnen worden omgezet in runtime-fouten. Geloof me niet? Overweeg dit:
Private mAccountName As String
Private Const ACCOUNT_NAME As String = "New User"
Private Sub Class_Initialize()
mAccountName = ACCOUNT_NAME
End Sub
Public Property Get MyAccountName() As String
MAccountName = Account_Name
End Property
Public Property Let MyAccountName(AccountName As String)
mAccountName = Account_Name
End Property
Als je snel naar de bovenstaande code kijkt, ziet het er vrij eenvoudig uit. Het is een klasse met een .MyAccountName eigendom. De lidvariabele voor de eigenschap wordt geïnitialiseerd op een constante waarde wanneer het object wordt gemaakt. Bij het instellen van de accountnaam in code, wordt de membervariabele opnieuw bijgewerkt. Bij het ophalen van de eigenschapswaarde retourneert de code alleen de inhoud van de lidvariabele.
Tenminste, dat is wat het moet doen. Als ik de bovenstaande code kopieer en in een VBA IDE-venster plak, wordt de behuizing van de identifiers consistent en verschijnen de runtime-bugs plotseling:
Private mAccountName As String
Private Const ACCOUNT_NAME As String = "New User"
Private Sub Class_Initialize()
mAccountName = ACCOUNT_NAME ' <- This is OK
End Sub
Public Property Get MyAccountName() As String
mAccountName = ACCOUNT_NAME ' <- This is probably not what we intended
End Property
Public Property Let MyAccountName(AccountName As String)
mAccountName = ACCOUNT_NAME ' <- This is definitely not what we meant
End Property
Implementatie:is dit echt de beste aanpak?
Uhh nee. Begrijp me niet verkeerd. Ik hou echt van het idee om automatisch het hoofdlettergebruik van identifiers te veranderen om de consistentie te behouden. Mijn enige echte klacht is dat de wijziging wordt aangebracht in elke identifier met die naam in het hele project. Veel beter zou zijn om het hoofdlettergebruik te veranderen van alleen die identifiers die naar hetzelfde "ding" verwijzen (of dat "ding" nu een functie, sub, eigenschap, variabele, enz. is).
Dus waarom werkt het niet op deze manier? Ik verwacht dat de VBA IDE-ontwikkelaars het eens zijn met mijn visie op hoe het zou moeten werken. Maar er is een zeer goede reden waarom de IDE niet zo werkt. Kortom, prestaties.
Helaas is er maar één betrouwbare manier om erachter te komen welke identifiers met dezelfde naam daadwerkelijk naar hetzelfde verwijzen:elke regel code ontleden. Dat is slooooowwww. Dit is meer dan een simpele hypothese van mijn kant. Het Rubberduck VBA-project doet eigenlijk precies dit; het parseert elke regel code in het project, zodat het geautomatiseerde code-analyse en een heleboel andere coole dingen kan doen.
Het project is weliswaar zwaargewicht. Het werkt waarschijnlijk prima voor Excel-projecten. Helaas ben ik nooit geduldig genoeg geweest om het in een van mijn Access-projecten te gebruiken. Rubberduck VBA is een technisch indrukwekkend project, maar het is ook een waarschuwend verhaal. Reikwijdte respecteren bij het wijzigen van hoofdlettergebruik voor identifiers zou leuk zijn om te hebben, maar niet ten koste van de huidige razendsnelle prestaties van VBA IDE.
Laatste gedachten
Ik begrijp de motivatie voor deze functie. Ik denk dat ik zelfs begrijp waarom het is geïmplementeerd zoals het is. Maar het is voor mij de meest gekmakende gril van VBA.
Als ik een enkele aanbeveling zou kunnen doen aan het Office VBA-ontwikkelteam, zou het zijn om een instelling in de IDE aan te bieden om automatische case-wijzigingen uit te schakelen. Het huidige gedrag kan standaard ingeschakeld blijven. Maar voor ervaren gebruikers die proberen te integreren met versiebeheersystemen, kan het gedrag volledig worden uitgeschakeld om te voorkomen dat hinderlijke "codewijzigingen" de revisiegeschiedenis vervuilen.