I have a generic question to see if anyone can help me with a better solution.
I have a .NET method that takes in 20+ boolean values, passed in individually.
For each parameter that is true I need to add a value to list.
Is there a more efficient way to add the values to the list besides have an if statement for each boolean?
Example:
Public Function Example(ByVal pblnBool1 as boolean, _
ByVal pblnBool2 as boolean, _
ByVal pblnBool3 as boolean)
If pblnBool1 then
list += "A"
End If
If pblnBool2 then
list += "B"
End If
End Function
Obviously this code isn't correct but it shows what I'm trying to do.
Anyone have any ideas?
Thanks
First off, having 20+ params sucks.
Secondly, you can use the ParamArray keyword to declare that you want the values passed to you in an array. (I don't think this is CLS compliant, meaning some languages won't be able to call your function without bundling the values into an array. But VB and C# can both easily work with each other's param arrays.) If you don't want to do that, you can always create the array yourself in your function. But i'd rather let the language and/or framework do that for me.
This is not optimized or anything; it's just an example.
sub Example(paramarray bools() as Boolean)
static vals() as String = {"A", "B", "C"}
if bools.Length > vals.Length then
throw new ArgumentException(String.Format( _
"Too many params! ({0} max, {1} passed)", _
vals.Length, bools.Length _
))
end if
for i as Integer = 0 to bools.Length - 1
if bools(i) then list += vals(i)
next
end sub
I assume that list is some member variable, since it's not defined in your code. If you intend for it to be the return value, then declare it in the function, and return it at the end. (And of course, turn sub Example(...) and end sub into function Example(...) as String and end function).
If you have 32 or fewer booleans to pass, you can use an instance of BitVector32. This allows to pass them all in a single integer. It provides methods for setting and retrieving the values.
Just make an array of boolean values and do a for...each loop through it. Or if you need to be selective, a 2-D array.
Related
I'm wondering if its possible to pass both lower and upper bound array sizes (using the "To" keyword) via arguments. Ultimately, I would like to do something like this:
sub foo
call bar(2 To 5)
end sub
sub bar(arrayDimensions)
dim myArray() as long
redim myArray(arrayDimensions)
end sub
But VBA throws a fit if I use the "To" keyword like this. Is there another easier alternative? Or am I doing something wrong? I know I could pass two arguments as a work around, but I would rather not do that if there's a better way.
Thanks in advance for any help.
Edit to clarify why I would like to do this instead of using two arguments.
I made an array class that I can use to store my arrays in and easily modify them (eg. myArray.fill(0)). But I want the user interface with this array class to be the same as just using a plan old array.
sub foo
dim regularArray() as long
redim regularArray(5)
regularArray(3) = 250
dim myArray as ArrayClass
set myArray = factory.newArrayClass(5)
myArray(3) = 250
end sub
This works great using default properties. My constructor is set up to either receive a one long argument to define the size of a 1D array. Or it can receive two long arguments to define the size of a 2D array. Or is can receive one range argument to build an array based on excel data. Or it can receive an array for its argument to instantiate with an array.
Right now my code works great as it is. But if I wanted to add a lower bound when I instantiate the class, lets say for a 2D array, then I have to add two more optional arguments, one for each dimensions.
That then leads to the question of: do two long arguments represent a 2D array, or does it represent the lower and upper bound of a 1D array. So it would get hairy in a hurry.
Passing two arguments is not a a work around, it is the way to do what you want.
Sub foo()
Call bar(2, 5)
End Sub
Sub bar(a As Long, b As Long)
Dim myArray() As Long
ReDim myArray(a To b)
End Sub
This works equally well, I guess:
Sub foo2()
Dim myArray(2 To 5) As Long
Call bar2(myArray)
End Sub
Sub bar2(myArray() As Long)
'Do stuff with myArray
End Sub
From a design point of view, it isn't clear why "foo" knows what dimensions are needed in "bar", and why "bar" can't set these dimensions (since "bar" is the function that is doing the work with "myArray" anyway). It is very unusual to see a construction like this. Also, for what its worth, I have never seen an array in practice that did not have a lower bound of either 0 or 1, so that is also a bit unusual.
You are trying to make things easier for your users which is to your credit. Unfortunately, this can make the programmers life a bit more complicated.
There are two alternative strategies.
1 use multiple constructors. Its not a sin to have multiple methods to construct your array so you might have methods such as
factory.newArrayClassBySize(5)
factory.newArrayClassOneDim(5,10)
factory.newArrayClassTwoDim(5,10,3,22)
factory.newArrayClassByArray(InputArray)
factory.newArrayClassByRange(inputXlRange)
This is, by far ,is my preferred method as it makes it very explicit what the code is doing.
An alternative strategy is to have 7 optional parameters where the name of the parameters makes it clear what the the constructor expects and will do. The limitation of this method is that users have to pay attention to the intellisense for the parameters
e.g.
Public Function newArrayClass _
( _
Optional Size as variant = Empty, _
Optional Lbound1 as variant = Empty, _
Optional Ubound1 as variant = Empty, _
Optional Lbound2 as variant = Empty, _
Optional Ubound2 as Variant = Empty, _
Optional XlRange as variant = Empty, _
Optional BasedOn as variant = Empty _
) as ArrayClass
Using the named parameters requires you todo more work to work out if a correct set of parameters has been provided.
I'm trying to set up a sub to be called upon and use the value of its result in the main sub. So far I've been using Function to carry over the value. However, I was wondering if there are any alternative ways of doing the same thing? I figured ByVal/ByRef is another way to do it by using a Sub instead of Function. My current codes are as follow:
Sub Main()
Dim i as Long
i = lr("A")
'some other calculations using i
End Sub
Function lr(Tar As String) As Long
Dim twb As Workbook
Set twb = ThisWorkbook
lr = ThisWorkbook.Sheets(1).Range(Tar & Rows.Count).End(xlUp).Row
End Function
My question is, How would I write this if I were to use a Sub instead of Function? Thanks!
So far I've been using Function to carry over the value.
Great, that's what functions are for! When you only need to return a single value, the best way is always going to be a function.
Things get fuzzier when you start needing to return two or more values. You could:
Use ByRef parameters and use them as "out" values.
This is "ok" for procedures (Sub), and confusing for functions - what determines which parameter gets to be the function's return value, and which parameters get to be passed ByRef? How does the calling code know whether or not to pass an initialized value to those ByRef parameters?
A naming convention can help here:
Public Sub Foo(ByVal foo1 As String, ByRef outBar1 As String, ByRef outBar2 As String)
An "out" prefix can tell the calling code that the parameter is an out value.
Scope the variables at a level that is accessible by both the caller and the callee.
This is a bad practice that can easily lead to spaghetti code. Avoid it - variables should have the smallest necessary scope possible, and be passed between methods/functions/procedures/modules, not just globally scoped and accessed by anyone at any given time!
Create a class to encapsulate all the values the function should return.
Definitely more object-oriented, results in much cleaner, readable code. The only downside is that VBA doesn't really encourage you to do this, and consistently doing that will result in a myriad of classes that you can't quite organize.
We have this utility method in a VB6 COM library for executing parameterised SQL:
Public Sub ExecSQL(ByVal strSQL As String, ParamArray varParams() As Variant)
'snip - ADODB data access
End Sub
Where varParams is a two dimensional array of SQL parameter information. Example usage would be:
ExecSQL("SELECT * FROM People WHERE Name = ?", Array("#p1", adVarChar, 10, "Smith"))
This is tried and tested code and works fine in normal usage. I am now in an unusual situation where the SQL string is a configurable value and could contain any number of parameters, so what I need to do is pass an unknown number of arguments in to the ParamArray. My attempt so far (simplified) is:
Function ExecConfigurableSql(sqlString As String, parameterValues() As String)
Dim parameters() As Variant
ReDim parameters(UBound(parameterValues)) As Variant
For i = 0 To UBound(parameterValues)
parameters(i) = Array("#p" + CStr(i), adVarChar, 1000, parameterValues(i))
Next
ExecSQL(sqlString, parameters) 'Type Mismatch
End Function
The attempt to execute the SQL throws a Type Mismatch error. Is there a way to pass an Array in to a function which expects a ParamArray? Or am I making an altogether separate mistake somewhere?
This is what the parameters look like with a dynamically built up array:
And this is what they look like when passed with comma-separated ParamArray syntax (which works):
The structure looks the same to me.
First, you never needed ParamArray in ExecSQL() in the first place as you're always passing one argument on the stack in addition to strSQL, i.e. an array of variants, which is
Array("#p1", adVarChar, 10, "Smith")
in the second listing. ParamArray is used to pass an undefined number or parameters on the stack, i.e. to be able to make calls like:
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1", adVarChar
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1", adVarChar, 10, "Smith"
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1"
ParamArray just take all the arguments passed on the stack and put them in an array for you.
So, you could have defined ExecSQL() as follow and it would have been the same thing provided your code get adapted to one-less Array() layer around varParams:
Public Sub ExecSQL(ByVal strSQL As String, varParams() As Variant)
' snip - ADODB data access '
End Sub
That being said:
Currently, the code in ExecConfigurableSql() transforms an array of strings (I presume field names), into the format expected by ExecSQL(), except that the (outer) array can (and will) contain more than one element of the sort Array("#p1", adVarChar, 10, "Smith").
Can ExecSQL() handle this? => Problem #1
[by the way, are all the fields 1000-char long??]
Problem #2: parameters is fine when you look at it from within ExecConfigurableSql(), but once you pass if to ExecSQL(), the ParamArray wraps it inside another array, so you really end up with (once in ExecSQL()) with something like this:
Now, you have to put the (unknown number of) parameters in an array, 'cause, well, you can't pass them on the stack to ParamArray since you don't know in advance the number of them. So you cannot remove the extra Array() wrapping from there.
You could get rid of the ParamArray in ExecSQL(), but that would break your existing ExecSQL() calls (for which varParams would only be wrapped once in an Array() instead of twice).
Knowing all this, you have two choices:
(1) Keep the declares as-is, and have ExecConfigurableSql() make multiple ExecSQL() calls within a For loop (btw, you declared it as a Sub so I presume it doesn't return any value); e.g.
Function ExecConfigurableSql(sqlString As String, parameterValues() As String)
For i = 0 To UBound(parameterValues)
Call ExecSQL(sqlString, Array("#p" + CStr(i), adVarChar, 1000, parameterValues(i))
Next
End Function
or
(2) Do the other way around, to improve the logic & consistency
Since you can still play with the declaration & implementation of ExecConfigurableSql(), change the second parameter to expect an array of Array("#p1", adVarChar, 10, "Smith")-like members (and not just field names).
Function ExecConfigurableSql(sqlString As String, varParamsArray() As Variant)
Dim varParams() As Variant
For i = 0 To UBound(varParamsArray)
varParams = varParamsArray(i)
' snip - ADODB data access '
Next i
End Function
Take the code from within ExecSQL() and put it in ExecConfigurableSql() where indicated - IMPORTANT: You have to update your code to account for the fact that parameters have one less Array() wrapped around them.
For ExecSQL(), remove the ParamArray keyword and treat the method as a special case of ExecConfigurableSql() where only one member is provided, i.e.:
Function ExecSQL(sqlString As String, varParams() As Variant)
Call ExecConfigurableSql(sqlString, Array(varParams))
End Function
Here's the code for the problem I'm having. It's pretty simple but I'm still learning.
I want to cache the result so the function returns a few seconds quicker than it currently is. At the moment it is returning to the caller in 4 when it should be 2.
Sub Main
console.writeline(getmyresult(2)) 'takes a while'
console.writeline(getmyresult(3)) 'takes a while'
console.writeline(getmyresult(2)) 'Should be instant'
console.writeline(getMyresult(3)) 'Should be instant'
End Sub
function getMyresult(X as interger) as integer
dim Y as integer=LongCompute(X)
return Y
end function
function LongCompute(X as integer) as integer
system.threading.thread.sleep(1000)
return x^2
end function
Any help would be greatly appreciated.
Yep, this is called memo-ization.
You can read up on it here:
http://en.wikipedia.org/wiki/Memoization
A simple implementation in visual basic would look like:
Dim dict As New Dictionary(Of Integer, Integer)
Sub Main()
console.writeline(getmyresult(2)) 'takes a while'
console.writeline(getmyresult(3)) 'takes a while'
console.writeline(getmyresult(2)) 'Should be instant'
console.writeline(getMyresult(3)) 'Should be instant'
End Sub
Function getMyresult(ByVal X As Integer) As Integer
If dict.ContainsKey(X) Then
Return dict(X)
Else
Dim temp = LongCompute(X)
dict.Add(X, temp)
Return temp
End If
End Function
Function LongCompute(ByVal X As Integer) As Integer
System.Threading.Thread.Sleep(1000)
Return x ^ 2
End Function
For a simple exercise you can put the results into a Dictionary, as James Culshaw suggested. The key is the input, the value is the cached result.
If this was for serious work I would rather consider using System.Runtime.Caching.MemoryCache. The problem with a dictionary is that items never get out of it (in a way they leak, altough if the input domain is bounded it's not that bad). A production-ready cache would handle memory pressure or support items expiration (e.g. cache the result for 10 minutes). Those requirements are handled by MemoryCache.
Caching the result of a function that has no side-effect and depends only on its inputs is formally called Memoization. A nice extension to your programming exercise would be to write a generic memoization function that can wrap any regular (slow) function. E.g. FastCompute = Memoize(SlowCompute).
Simple option would be to use a Dictionary object and then check to see if the key has been set for the parameter passed into getmyresult. If it has, pass the value stored in the dictionary, if not process the result, add it to the dictionary, and then return the result. second call will be near instantaneous as its already cached in the dictionary.
In what situtations is it best to use reference arguments to return values?
Sub Example(byref value as integer)
value = 3
End Sub
In what situations is it best to return the value (perhaps in a structure for more complex types)?
Function Example() as integer
return 3
End Function
In general, I'd avoid using reference arguments to return values.
THe design guidelines suggest avoiding this, which is why the Microsoft code analysis tools warn you when they find it.Do not pass types by reference.
It's nearly always more maintainable to return values instead of passing arguments by reference, unless there is a very specific need to do so. If you're generating a new value, return it.
when you want to return a state or status of an operation plus the result from the operation.
think of TryParse..it returns a conversion result as true or false and it returns the converted value by a ref variable.
Dim number As Integer
Dim result As Boolean = Int32.TryParse(value, number)
Public Shared Function TryParse ( _
s As String, _
<OutAttribute> ByRef result As Integer _
) As Boolean
but other than that, as others suggested i would not use by ref a lot, it can make code very hard to read and debug.
It really depends what the function's doing. Generally, though, if there's only one return, by value is easier for the caller. They can simply do:
int foo = Example(foo)
or:
int modifiedFoo = Example(foo)
as they prefer.