I’m working on a project which is written in VB.NET. The project has several Structures which used to have writable fields. I replaced all of those fields with read-only properties, and wrote functions for creating a copy of a structure that has one of its properties changed.
I was assuming that every part of the code that attempted to write to one of these properties would become an error, and then I could simply fix all the errors by making the code call the new functions. To my dismay, it turns out that if a ReadOnly property is accidentally passed into a ByRef parameter of a function, the compiler accepts this with no warning, and the value that’s assigned is silently discarded!
Here’s an example:
Structure Point
Public ReadOnly Property X As Integer
Public ReadOnly Property Y As Integer
End Structure
Module Module1
Sub IncreaseByOne(ByRef x As Integer)
x = x + 1
End Sub
Sub Main()
Dim point As New Point
IncreaseByOne(point.X)
Console.WriteLine($"point.X is {point.X}")
End Sub
End Module
I was hoping that the line IncreaseByOne(point.X)
would throw an error, or at least a warning, since point.X
is read-only and it doesn’t make sense to pass it by reference. Instead, the code compiles with no warnings, and the value assigned to x
inside of IncreaseByOne
is silently discarded, and the program prints point.X is 0
.
How can I detect all of the places in my code where a read-only property is passed into a function that takes it by reference? The only way I can think of is to go through every read-only property that I have, find all places where that property is used as a parameter, and look to see if that parameter is ByRef. That’ll be very time-consuming, but if there’s no other solution, then that’s what I’ll do.
I’m using Visual Studio 2019. I’m open to installing new software in order to do this.
3
Answers
As Craig suggested in this answer, I went ahead and wrote a custom analyzer to detect when this occurs. Now, I can simply do Analyze / Run Code Analysis / On Solution, and every place that the described problem occurs gets marked with a warning such as "The property 'point.X' is read-only and should not be passed by reference."
The entire analyzer is available on GitHub. I've copied the important part below:
There isn’t a way to catch this with the compiler. Even
Option Strict On
will allow passing a read-only property to aByRef
argument. This is defined to pass by copy-in/copy-out, and it’s surprising to me that the copy-out part will compile even when theProperty Set
is inaccessible.If you want to have an automated lint-type check for this, I would imagine that a custom analyzer could find it. I haven’t worked with analyzers, so I don’t have any specific suggestions for how to write one or set it up.
Otherwise, you’re left to a manual check. As was noted in a comment, you can use the "Find All References" command from Visual Studio to help with it, but this will still require a manual review of every read-only property.
That’s really interesting. The VB.NET Compiler really tries to make a property look like a variable. Even if I explicitly declare the property as
The code compiles and executes as before. If the property setter is added, it even works correctly!
With the above change, the program correctly prints 1.
Looking at the generated IL, we can see why:
Even though we expect an error because a property is not a value (and a byref requires a value), the compiler fakes what we might have intended: He actually generates a call to the getter, stores the value on the stack, passes a reference to the stack(!) to the called function and then calls the setter with that value.
This works in this simple scenario, but I agree with the commenters above, this might be very confusing when looking at it in detail. If the property is actually a computed property, the outcome is just arbitrary (try implementing the getter as
Return _x + 1
…)C# would throw an error here, because a property is not a value and hence cannot be used as an out or ref parameter.