So I have a UDT set up like this:
Public Type UserInfo
name as string
username as string
active_time as double
End Type
I then create an array of this type:
Dim list_of_users() as UserInfo
'Populate array here
What I want to do is pass the active_time values as an array into a separate function. Something like:
'StdDev function defined elsewhere
standard_dev_all = StdDev(list_of_users().active_time)
Is this even possible? I suppose I could modify the function to deal with my UDT, but I have many more values than just active_time and it seemed like that would make it pretty messy.
You cannot pass it as
UDTvariable.Element
' or
UDTvariable().Element
into a function. I mean,
the first one is not valid as the index of the array element (not the element's Element) hasn't been specified.
The second one is invalid as well.
The solution is to pass on to your function
Answer = Stdev(UDTVariable)
and inside the function, you do this
Function Stdev(ByRef UDTVariableTypeName UDTVariable)
for i = 1 to N
Something = UDTVariable(i).Element
' so on and so forth
next i
Stdev = SomeAnswer
End Function
You may omit writing ByRef as that is the default way of passing arguments, but I've kept it for the sake of clarity.
Related
I was wondering if there is any way to access the expected data type within a function similar to an event arg. I am doubtful that this is possible, though it would be an excellent feature.
I frequently work with (old and disorganized)Mysql databases creating interfaces through VB.Net. Often I will have an optional field which contains a NULL value in the database. I am frequently dealing with errors due to NULL and dbnull values in passing data to and from the database.
To complicate things, I often am dealing with unexpected datatypes. I might have an integer zero, a double zero, an empty string, or a string zero.
So I spend a fair amount of code checking that each entry is of the expected type and or converting NULLs to zeros or empty strings depending on the case. I have written a function ncc(null catch convert) to speed up this process.
Public Function ncc(obj As Object, tp As Type) As Object 'Null Catch Convert Function...
My function works great, but I have to manually set the type every time I call the function. It would be so much easier if it were possible to access the expected type of the expression. Here is an example of what I mean.
Dim table as datatable
adapter.fill(table)
dim strinfo as string
dim intinfo as long
strinfo = ncc(table.Rows(0).Item(0),gettype(String)) 'here a string is expected
intinfo = ncc(table.Rows(0).Item(0),gettype(Long)) 'here a long is expected
It would be so much more efficient if it were possible to access the expected type directly from the function.
Something like this would be great:
Public Function ncc(obj As Object, optional tp As Type = nothing) As Object
If tp Is Nothing Then tp = gettype(ncc.expectedtype)
That way I do not have to hard code the type on each line.
strinfo = ncc(table.Rows(0).Item(0))
You can make the ncc function generic to simplify calling it:
Public Function ncc(Of T)(obj As T) As T
If DbNull.Value.Equals(obj) Then Return Nothing
Return Obj
End Function
This kind of function will be able to in some cases infer the type, but if there's any possibility of null you'll still want to include a type name (because DBNull will be the inferred type for those values). The advantage is not needing to call gettype() and so gaining a small degree of type safety:
strinfo = ncc(Of String)(table.Rows(0).Item(0))
But I think this has a small chance to blow up at run time if your argument is not implicitly convertible to the desired type. What you should be doing is adding functions to accept a full row and return a composed type. These functions can exist as static/shared members of the target type:
Shared Function FromDataRow(IDataRow row) As MyObject
And you call it for each row like this:
Dim record As MyObject = MyObject.FromDataRow(table.Rows(i))
But, you problem still exists.
What happens if the column in the database row is null?
then you DO NOT get a data type!
Worse yet? Assume the data column is null, do you want to return null into that variable anyway?
Why not specify a value FOR WHEN its null.
You can use "gettype" on the passed value, but if the data base column is null, then you can't determine the type, and you right back to having to type out the type you want as the 2nd parameter.
You could however, adopt a nz() function (like in VBA/Access).
So, this might be better:
Public Function ncc(obj As Object, Optional nullv As Object = Nothing) As Object
If obj Is Nothing OrElse IsDBNull(obj) Then
Return nullv
End If
Return obj
End Function
So, I don't care if the database column is null, or a number, for such numbers, I want 0.
So
dim MyInt as integer
Dim MyDouble As Double
MyInt = ncc(rstData.Rows(0).Item("ContactID"), 0)
MyDouble = ncc(rstData.Rows(0).Item("ContactID"), 0)
dim strAddress as string = ""
strAddress = ncc(rstData.Rows(0).Item("Address"), "")
Since in NEAR ALL cases, you need to deal with the null from the DB, then above not only works for all data types, but also gets you on the fly conversion.
I mean, you CAN declare variables such as integer to allow null values.
eg:
dim myIntValue as integer?
But, I not sure above would create more problems than it solves.
So,
You can't get exactly what you want, because a function never has knowledge of how it's going to be used. It's not guaranteed that it will be on the right-hand side of an assignment statement.
If you want to have knowledge of both sides, you either need to be assigning to a custom type (so that you can overload the assignment operator) or you need to use a Sub instead of an assignment.
You could do something like this (untested):
Public Sub Assign(Of T)(ByVal field As Object, ByRef destination As T,
Optional ByVal nullDefault As T = Nothing)
If TypeOf field Is DBNull Then
destination = nullDefault
Else
destination = CType(field, T)
End If
End Sub
I haven't tested this, so I'm not completely certain that the compiler would allow the conversion, but I think it would because field is type Object. Note that this would yield a runtime error if field is not convertible to T.
You could even consider putting on a constraint requiring T to be a value type, though I don't think that would be likely to work because you probably need to handling String which is a reference type (even though it basically acts like a value type).
Because the destination is an argument, you wouldn't ever need to specify the generic type argument, it would be inferred.
Let's say you are using a pre-defined function that has return parameters that are not needed. To clarify, either the function doesn't need the value or you don't need the result or both.
Is there any drawback or problem with using a constant in the call? Or is it better to create a temporary variable just for the purposes of filling out the call?
The goal is to reduce variable definitions in the calling program. The subroutine definition cannot be changed. The question is whether supplying a constant is advisable/recommended in this case.
Dim res As Double = 2.0#
''' do not need second/third results, third value isn't used
''' calling the function this way saves creating two extra variables
Call AddOneTwoThree(res, 3.0#, 0.0#)
''' this function cannot be changed
Public Sub AddOneTwoThree(ByRef first As Double, ByRef second As Double, ByRef third As Double)
first = first + second
second = second + third
third = third
End Sub
So I think the comments provided by tntinmn and jmcilhinney are good answers from my perspective.
0.0# or Nothing can be specified for a ByRef Double parameter if the result is not needed assuming values of 0.0# does not adversely affect the output.
I tested with Nothing and the result is the same as if 0.0# was specified.
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
I have a bunch of type Double variables in my program, say for example
Dim Area as Double = 0
Dim Perimeter as Double = 0
Somewhere in my program I want to calculate these values, so I define
Public Sub TheSquare(ByRef TheArea as Double, ByRef ThePerim as Double, ByVal TheSide as Double)
TheArea = TheSide^2
ThePerim = 4 * TheSide
End Sub
and somewhere in the program I'm collecting side lengths and calculating the area and perimeter; say
While True
S = GetSideValueFromSomewhere()
TheSquare(Area, Perimeter, S)
End
In my real program, I have, say, 20 quantities that I want to calculate. Obviously each one has a different equation. But in the end I want to output all 20 to a file, so to save typing, I create an array of the quantities, like this:
Dim TypingSaver() as Double = {Area, Perimeter}
so that I can dump values to file with a three-line for-loop instead of copying and pasting 20 variable names.
This does exactly what I want if Area and Perimeter were reference types like Objects. But since they are Doubles, TypingSaver contains only their values, not references to the actual variables. So after I use my TheSquare function the values of Area and Perimeter are correctly updated but TypingSaver just constains whatever the values of Area and Perimeter were when I declared the array.
So the question is: how can I create an array of references to doubles in VB.NET?
With approach that you are doing you can't do this, since as soon as you created array you copied all variables to the array and any changes that you are doing on variables are not reflected on array variables (like you pointed out).
What I would recommend create another class that will contain all your variables (20 variables name) as properties (get and set) and then override ToString method which will return list of all your variables. So when you need to dump those variables you will call ToString() method and it will return current values of all your parameters.
There is a nasty way to do this. All numeric types are value types, but arrays are reference types. So I can change all my Doubles to arrays of doubles, like this:
Dim Area(0) as Double
Dim Perimeter(0) as Double
So now Area and Perimeter are 1-element arrays of double. My "looping array" then becomes
Dim TypingSaver() as Array = {Area, Perimeter}
Now TypingSaver stores references to Area and Perimeter. For me this was an easy change because I could search-and-replace for the Double declaration, change the type for TypingSaver, then in two other places I had to change direct access of this form:
TypingSaver(1) = 7
to
TypingSaver(1).SetValue(7, 0)
Not pretty, but it keeps my code consistent in that I have other "looping arrays" for other objects that are all related to each other.
Although it is not clear in my question the real solution is to restructure everything so instead of storing everything in arrays I crate a class which has all the objects I need and create a single array of that, as suggested in part by Blezden.
The single-element array idea was actually terrible. The code became horrendously slow. There is another work around a friend suggested: create a wrapper class like this:
Public Class DoubleWrapper
Public Value As Double
End Class
Then when an array of DoubleWrappers is created it will be by reference, of course.
I understand this is not ideal but what I was looking for was a workaround until I have the time to rewrite the code from scratch.
Fairly similar to your most recent answer, you'll want to wrap your types as "Nullable", which basically makes it an object that could be null, but also a reference type.
Ie. Dim testDouble as Nullable(Of Double) or Dim testDouble2 as Double?
See:
Nullable(Of T) Structure - MSDN
What is the integer reference type in C#?
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.