An Exhausting List of Differences Between VB.NET & C#

For almost half of my lifetime now I’ve witnessed or engaged in countless discussions about how similar or different the two most popular .NET languages are. First as a hobbyist, then as a professional, and finally as a customer advocate, program manager, and language designer I cannot exaggerate the number of times I’ve heard or read some variation of:

“…VB.NET is really just a thin skin on top of IL, like C#…”


“…VB.NET is really just C# without semicolons…”

As though the languages were an XML transform or a style sheet.

And when some impassioned passer-by isn’t saying it on a blog comment it’s just as often implied by the premise of a question like:

“Hey, Anthony! I ran into this one little difference in this one corner–is this a bug? How could these two otherwise identical languages that should by all that is good and holy in the world be identical have diverged in this one place. Why has this injustice been visited upon us!?”

diverged” like once they were the same until a mutation occurred and then they were separate species. LOL

And I get it. Before I joined Microsoft I might have vaguely held that idea too and used it in arguments to push back against detractors or reassure someone. I understand its allure. It’s easy to grasp and very easy to repeat. But, working on Roslyn–the complete from-the-ground-up rewrite of both VB and C#–for 5 years, I came to understand how unequivocally false this idea really is. I worked with a team of developers and testers to re-implement from scratch every inch of both languages as well as their tooling in a huge multi-project solution with millions of lines of code written in both. And with so many developers switching back and forth between them, and a high bar for compatibility with the outputs and experiences of previous versions, and the need to faithfully represent every detailed nuance throughout a massive API surface area I got to be intimately familiar with the differences. In fact, at times it felt like I learned something new about my VB.NET (my favorite language) every day.

Well, finally, I’ve made the time to sit down and brain-dump a fraction of what I’ve learned using and creating VB.NET for the last 15 years, in the hopes that I can at least save myself time next time it comes up.

Before I get to the list I’ll lay down the ground rules:

  • This list is not exhaustive. It’s exhausting. It’s not all the differences there are, it’s not even all the differences I’ve ever known, it’s just the differences I can remember off the top of my head until such time as I become too tired to go on; until I’m exhausted. If I or others run into or remember other differences I’ll gladly update this list after the fact.
  • I will start at the top of the VB 11 Language Specification and work my way down, using its content to remind me of my top-of-mind differences on that topic.
  • This is NOT a list of features in VB that aren’t in C#. So no “XML Literals vs Pointers”. That’s fairly low-hanging fruit and there’s tons of lists like that online already (some of which were written by me and maybe I’ll write another one in the future). I’ll be focusing primarily on constructs where there’s an analog in both languages where an uninformed observer might assume those two things behave the same but where there are either subtle or grand differences; these may look the same but they don’t work the same or generate the same code at the end of the day.
  • This is NOT a list of syntax differences between VB and C# (of which there are countless). I’ll mostly be talking about semantic differences (what things mean), rather than syntactic ones (how things are written). So no “VB starts comments with ‘ and C# uses //” or “In C# _ is a legal identifier and it’s not in VB” type stuff. But I will break this rule for a handful of cases; the first section of the spec is about lexical rules, after all.
  • Often enough I’ll provide an example, and occasionally I’ll suggest a rationale for why a design might go one way or the other. Some designs happened on my watch, but the vast majority predate my time and I can only speculate as to why.
  • Please leave a comment or give me a shout on Twitter (@ThatVBGuy) to let me know your favorite differences and/or ones you’d like to dive into further.

Having set those expectations and without further delay…

Table of Contents

Syntax & Pre-processing
1. VB keywords and operators can use full-width characters
2. VB supports “smart-quotes”
3. Pre-processing constants can be of any primitive type (including Dates) and hold any constant value
4. Pre-processing expressions can use arbitrary arithmetic operators

Declarations, etc.
5. VB sometimes omits implements declarations in IL to prevent accidental implicit interface implementation by name
6. VB hides base class members by name by (Shadows) default, rather than by name-and-signature (Overloads)
7. VB11 and under are more protective of Protected members in generics
8. “Named argument” syntax in attributes always initializes properties/fields
9. All top-level declarations are (usually) implicitly in the project root namespace
10. Modules are not emitted as sealed abstract classes in IL so they don’t appear exactly as C# static classes and vice versa.
11. You don’t need an explicit entry-point (Sub Main) method in WinForms apps
12. If you call certain legacy VB runtime methods (e.g. FileOpen, the calling method will implicitly be decorated with an attribute to disable inlining for correctness reasons
13. If your type is decorated with the DesignerGenerated attribute and doesn’t include any explicit constructor declarations, the default one the compiler generates will call InitializeComponent if it’s defined on that type.
14. Absence of the Partial modifier does NOT mean that type is not partial
15. The default accessibility in classes is Public for everything but fields, and Public for fields as well in structures
16. VB initializes fields AFTER the base constructor call whereas C# initializes them BEFORE the base constructor call
17. The implicitly declared backing field of VB events have a different name than they do in C# and are accessible by name
18. The implicitly declared backing field of VB auto-implemented properties have a non-mangled name in VB and are accessible by that name
19. The implicitly declared backing field of VB read-only auto-implemented properties are writeable
20. Attributes on events sometimes apply to the event backing field

21. The scope of a label is the body of the entire method containing it; you can jump into blocks (w/ exceptions)
22. Local variable lifetime <> variable scope
23. Variables are always initialized with the default value for their type
24. RaiseEvent does NOT throw an exception if the event backing field is null
25. Assignments aren’t always the same; sometimes assigning a reference type makes a shallow clone
26. Select Case does not support fall-through; no break required
27. Case blocks each have their own scope
28, 29, 30. Select Case works on non-primitive types, can use arbitrary non-constant expressions in tests, and uses the = operator by default
31. Variables declared inside loops retain their value between iterations, sort of
32. The three For loop expressions are only ever evaluated once at the beginning of the loop
33. For Each loops in VB can use extension method GetEnumerator

34. Boolean conversions
35. Conversions between Enum types, and between Enum types and their underlying types are completely unrestricted, even when Option Strict is On
36. Overflow/underflow checking for integer arithmetic is entirely controlled by the compilation environment (project settings) but VB and C# use different defaults; overflow checking is on by default in VB
37. Conversions from floating-point numbers to integral types using bankers rounding rather than truncating
38. It is not an error to convert NotInheritable classes to and from interfaces they do not implement at compile-time
39. Attempting to unbox a null value to a value type produces the default value of the type rather than a NullReferenceException
40. Unboxing supports primitive conversions
41. There are conversions between String and Char
42. There are conversions between String and Char array
43 & 44. Conversions from String to numeric and Date types support the language literal syntax (mostly)
45. There are NO conversions between Char and integral types

46. Nothing <> null
47. Parentheses affect more than just parsing precedence; they re-classify variables as values
48. Me is always classified as a value—even in structures
49. Extension methods can be accessed by simple name
50. Static imports will not merge method groups
51 & 52. Partial-name qualification & Smart-name resolution
53. Collection initializer Add methods may be extension methods
54. Array creation uses upper-bound rather than size
55. VB array literals are f-ing magic not the same as C# implicitly typed array creation expressions
56. Anonymous type fields can be mutable AND are mutable by default
57. Neither CType nor DirectCast are exactly C# casting
58. The precedence of certain “equivalent” operators is not exactly the same
59. String concatenation is not the same + and & are not the same with regard to String concatenation and + in VB <> + in C#
60. Division works sanely: 3 / 2 = 1.5
61. ^ isn’t exactly Math.Pow
62. Operator =/<> are never reference equality/inequality
63. Operator =/<> on strings are not the same (or any relational operators for that matter)
64. Nullable value types use three-valued logic (null propagation in relational operators)
65. Overloaded operators don’t always have a 1:1 mapping
66. Function() a = b is not the same as () => a = b
67. An Async Function lambda will never be interpreted as an async void lambda
68. Queries are real(er) in VB
69 & 70. As clause in query From doesn’t not always call cast; bonus from ‘As’ clause can perform implicit user-defined conversions
71-75. Select clause isn’t required at all, can appear in the middle of the query, can appear multiple times, and can declare multiple range variables with implicit names, or explicit names

Syntax & Pre-processing

1. VB keywords and operators can use full-width characters

In some languages (I do not know how many), but at least some forms of Chinese, Japanese, and Korean, full-width characters are used. You can read about that in detail here but what it means in short is that when using a fixed-width font (as most programmers do) a Chinese character will take up twice the horizontal space as the Latin characters you’re used to seeing in the west. For example:

I have here a variable declaration written in Japanese and initialized with a string, also written in Japanese. According to Bing translate the variable name is ‘greeting’ and the string says “Hello World!”. The variable name is only 2 characters in Japanese but it occupies the space of 4 of the half-width characters my keyboard normally produces as indicated by the first comment. There are full-width versions of the numbers and all the other printable ASCII characters that occupy the same width as the Japanese characters. I’ve written the second comment using the full-width numbers “1” and “2” to show this. That’s not the same “1” and “2” used in the first comment. There are no spaces between the numbers. You can also see that the characters are not exactly 2 characters wide. There’s some slight offset there. In part this is because that program is mixing full-width and half-width characters in the same line and across all three lines. The spaces are half-width, the alphanumeric characters are full-width. We programmers are nothing if not obsessive about our text alignment so I can imagine that if you’re Chinese, Japanese, or Korean (or anyone else using full-width characters in their language) and using identifiers or strings written natively in those character sets, that these little alignment errors are infuriating.

As I understand it, depending on your Japanese keyboard, it’s easy to switch between kanji and Latin characters but it’s preferable to use full-width Latin characters. VB supports this with the keywords, the spaces, the operators, even the quotes. So this could all be written thus:

As you can see, in this version the keywords, the spaces, the comments, the operators, even the quotation marks are using their full-width versions. Order has been brought to chaos.

Yes. Japanese people use VB. In fact, despite (or maybe because of) the English-like syntax the majority of VB users I see on forums speak a non-English language as their first and I met several Japanese VB MVPs during my time at Microsoft, at least one of whom always brought Japanese candy. If you’re a Chinese, Japanese, or Korean VB programmer (or any other language that uses full-width characters), please leave a comment.

Fun trivia: When I originally implemented interpolated strings in VB I (shamefully) failed to allow for full-width curly braces inside the expression holes. Vladimir Reshetnikov found this bug and implemented the fix so that VBs proud tradition of width-tolerance would remain upheld.

2. VB supports “smart-quotes”

OK, this one is kinda cheap but it’s worth calling out. Have you ever read a code sample in a word document like this:

And then tried to copy the sample into your code only to find that none of the (highlighted) quotation marks work because Word changes all the regular ASCII quotes to “smart-quotes”?

Well, I haven’t. OK, I have, but only when copying C# sample code. In VB smart quotes are legal delimiters for strings:

They also work inside the strings in an arguably wonky way though. If you double-up on the smart quotes to escape them what you get in your string at run-time is just a regular (dumb) quote. It’s a bit odd when you think about it yet practical as all get out since almost nothing would actually expect to see a smart quote in the string. The compiler does NOT enforce that if you start with a smart quote that you end with one or that you use the correct one so you can mix and match however and not get confused. And yes, it also works with the single-quote character used for comments:

I tried to get Paul Vick to confess that he did it precisely because he got tired of this problem when working on the spec but he denies culpability. This didn’t exist in VB6 so someone added it since then.

3. Pre-processing constants can be of any primitive type (including Dates) and hold any constant value

4. Pre-processing expressions can use arbitrary arithmetic operators

Declarations, etc.

5. VB sometimes omits implements declarations in IL to prevent accidental implicit interface implementation by name

This one is pretty esoteric. In VB interface implementation is always explicit. But it turns out that in the absence of an explicit mapping, the CLR’s default behavior is to look for public methods matching an interface member’s name and signature when invoking it. In most cases, this is fine because in VB you’re usually required to provide an implementation for every member in an interface you implement; except in this case:

In this example the class FooDerived only wants to re-map IFoo.Bar to a new method but leave the other existing mapping untouched. It turns out that if the compiler simply emitted the implements declaration for FooDerived, the CLR would also pick-up FooDerived.Baz as the new implementation of IFoo.Baz (though in this example it’s unrelated). In C# this happens implicitly (and I’m not sure how to opt out of it) but in VB the compiler actually omits the ‘implements’ from the entire declaration to avoid this and only overrides the specific members being re-implemented. In other words, if you ask FooDerived if it implements IFoo directly, it will say ‘no’:

Why do I know this and why is this important? Well, for years VB users have request support for implicit interface implementation (without an explicit Implements clause on every declaration) usually for code-generation scenarios. In part because of this just enabling it with the existing syntax would be a breaking change because FooDerived.Baz would now implicitly implement IFoo.Baz where it didn’t before. But most recently I was educated about this behavior when discussing potential design issues with the default interface implementation feature which would allow interfaces to include default implementations of some members that don’t need to be re-implemented for every class that implements the interface. This would be useful for overloads, for example, where the implementation is highly likely to be the same in all implementers (delegating to the main overload). Another scenario is versioning. If an interface can include default implementations you can add new members to it without breaking older implementers. But here’s where the problem comes in. Because the default behavior in the CLR is to look for public implementations by name and signature, if a VB class didn’t implement the interface members with default implementations but had existing public members with the right name and signature they would implicitly implement those default members, even if doing so was completely unintentional. There are things that can be done to work-around this when the full set of members to the interface is known at compile-time. But in the scenario where a member was added after the code was compiled it would simply silently change behavior at run-time.

6. VB hides base class members by name by (Shadows) default, rather than by name-and-signature (Overloads)

This one is fairly well-known, I think. But the scenario is this, you inherit a base class (DomainObject), possibly outside of your control, and you declare a method with a name that makes sense in the context of your class , e.g. Print:

It makes sense that an Invoice could be printed. But in a future version of the API the declares your base class decides that for debugging purposes they’re adding a method to all DomainObjects that writes the full contents of that object out to the debug window. This method is brilliantly named Print. The problem is that a consumer of your API could see that Invoice object has Print() and Print(Integer) methods and think they’re related overloads. Perhaps the former just prints 1 copy. But that’s not at all what you, the author of Invoice, intended. You had no idea that DomainObject.Print would come into existence. So, yeah, it doesn’t work like that in VB. There’s a warning when this scenario pops up, but more importantly, the default behavior in VB is hide-by-name. Meaning unless you explicitly state that your Print is an overload of your base class Print via the Overloads keyword, the base class member (and any overloads it may have) are hidden entirely. Only the API you defined originally is presented to consumers of your class. That’s the default but you can make it explicit via the Shadows keyword. C# only has the Overloads capability (thought it will respect it if referencing a VB library) and that is its default (expressed with the new keyword). But this does come up from time to time when certain inheritance hierarchies appear in people’s projects where one class is defined in one language and another in the other and there are methods being overridden but that’s beyond the scope of this bullet point.

7. VB11 and under are more protective of Protected members in generics

We actually changed this between VS2013 and VS2015. Specifically we decided to not bother re-implemented it. But I’m writing it down in case you’re on an older version and notice it. In short, given a Protected member declared in a generic type, a derived type that is also generic may only access that protected member through a derived instance with the same type arguments.

We thought the rule was over-protective (no pun intended) and dropped it in Roslyn.

8. “Named argument” syntax in attributes always initializes properties/fields

VB uses the same syntax := for initializing attribute properties/fields as for passing method arguments by name. Consequently there’s no way to pass an argument to the constructor of an attribute by name.

9. All top-level declarations are (usually) implicitly in the project root namespace

This one is almost in the “extra feature” category, but I include it because it does change the meaning of code. In the VB project properties there is a field:

By default it’s just the name of your project when you create it. It is NOT the same as the box in the C# project properties called “Default namespace”. The default namespace is just what code is spit into new files by default in C#. But VBs root namespace means that, unless otherwise stated, every top-level declaration in this project is implicitly in this namespace. That’s why the VB item templates don’t usually include any namespace declaration. Furthermore, if you do include a namespace declaration it isn’t overriding the root but adding to it:

Namespace Controllers
' Child namespace.
End Namespace
Namespace Global.Controllers
' Top-level namespace
End Namespace

So namespace Controllers is actually declaring a namespace VBExamples.Controllers unless you escape this mechanism by explicitly rooting your namespace declaration in the Global namespace.

This is convenient because it saves every VB file 1 level of indentation and one extra concept. And it’s extra useful if you’re making a UWP app (because everything must be in a namespace in UWP) and extremely convenient if you decide to change the top-level namespace for your entire project, say from some code name like Roslyn to a longer release name like Microsoft.CodeAnalysis so you don’t have to manually update every file in the solution. It’s also important to keep in mind when dealing with code-generators and XAML namespaces and the new .vbproj file format.

10. Modules are not emitted as sealed abstract classes in IL so they don’t appear exactly as C# static classes and vice versa.

VB Modules pre-exist C# static classes, though we did try to make them look the same in IL in 2010. Sadly this was a breaking change because the XML Serializer (or maybe it was the binary one) for that version of .NET (I think they fixed it) won’t serialize a type nested in a type that can’t be constructed (which abstract types can’t be). It throws an exception.

We discovered this after making the change and reverted it because some code somewhere used an enum type which was nested inside a module. And since you can’t know which version of the serializer will be run against a compiled program it can never be changed as it would work in one version of an app and throw exceptions in other ones.

11. You don’t need an explicit entry-point (Sub Main) method in WinForms apps

If your project sets a Form as the start-up object and does not use the “Application Framework” (more on that in a future post) VB will synthesize a Sub Main that instantiates your startup form and passes it to Application.Run, thus saving you a separate file to manage this, or an extra method in your Form, or ever having to think about this problem.

12. If you call certain legacy VB runtime methods (e.g. FileOpen, the calling method will implicitly be decorated with an attribute to disable inlining for correctness reasons

In short, VB6 style file I/O methods like FileOpen rely on tracking state specific to the assembly the code appears. For example file #1 may be a log in one project, and a config file in another; what (1) means is contextual. To determine what assembly is running Assembly.GetCallingAssembly() is called. But if the JIT inlines your method into your caller from the perspective of the stack the VB runtime method is being called by your caller, not your method, which may be in a different assembly, which then could allow your code to access/corrupt private state from your caller. It’s not a security solution because if compromised code is running in process you’ve already lost, but it is a correctness one. So if you’re using these methods the compiler disables inlining.

This was a last minute (and thus stressful) change made in 2010 because the x64 JIT is VERY aggressive at inlining/optimizing code and we discovered it very late in the cycle and this was the safest option.

13. If your type is decorated with the DesignerGenerated attribute and doesn’t include any explicit constructor declarations, the default one the compiler generates will call InitializeComponent if it’s defined on that type.

In the age before Partial types existed, a war was waged by the VB Team to lower the boilerplate in VB WinForms projects. But even with Partial this solves a problem because it allows the generated file to omit the constructor entirely so that a VB user may manually declare one if they needed in their own file or not as needed. Without this the designer would need to add the constructor just to invoke InitializeComponent and if the user added one they’d be duplicates, or the tooling would need to be smart enough to move the constructor over from the designer file to the user file, and to not re-generate it in the designer if it exists in the user file.

14. Absence of the Partial modifier does NOT mean that type is not partial

Technically in VB only one class has to be marked Partial. This is usually (in GUI projects) the generated file.

Why? It keeps the user file nice and clean and can be very convenient for opting into generation after the fact, or augmenting generated code with user code. However, the advice is that at most one class omit the Partial modifier and if more than one definition omits it a warning is reported.

15. The default accessibility in classes is Public for everything but fields, and Public for fields as well in structures

Mixed feelings about this. In C# everything is private by default (Yay, encapsulation!) but there’s an argument to be made based on what you’re more often declaring, your public contract or your implementation details. Properties and events are usually for public consumption and operators can’t be anything but public. I rarely rely on default accessibility myself though (except in demos, like all of the examples on this page).

16. VB initializes fields AFTER the base constructor call whereas C# initializes them BEFORE the base constructor call

You know how “they” say that the first thing that happens in a constructor is a call to the base class constructor? Well, it’s not, at least in C#. In C#, before the call to base(), whether implicit or explicit, the initializers on fields execute first, then the constructor call, then your code. There are implications to this design decision and I think I know why a language designer would go one way or the other. I believe on such implication is that this code can’t be translated into C# directly:

Imports System.Reflection
Class ReflectionFoo
Private StringType As Type = GetType(String)
Private StringLengthProperty As PropertyInfo = StringType.GetProperty("Length")
Private StringGetEnumeratorMethod As MethodInfo = StringType.GetMethod("GetEnumerator")
Private StringEnumeratorType As Type = StringGetEnumeratorMethod.ReturnType
Sub New()
End Sub
End Class

Back in my Reflection-slinging days I used to write code like this a lot. And I vaguely recall a pre-Microsoft co-worker (Josh) who would translate my code to C# sometimes complaining about having to move all my initializer code into the constructor. In C# it’s illegal to reference the object under construction before the base() call. And since field initializers run before said call it’s also illegal for field initializers to refer to other fields, or any instance members of the object, actually. So this example also only works, as written, in VB:

Here, we have a base class that presumably has a bunch of functionality in it but it needs some key object to manage, operate on, that is specified by a derived type. There are many many ways to write this pattern, but this is the one I usually go for, because:

  • It’s short
  • Doesn’t require me to declare a constructor
  • Doesn’t require me to put initialization code in the constructor if there is one
  • Allows me to cache the created object and doesn’t require derived types to declare and manage storage for the object provided, though now with auto-props that’s far less of an issue.

Now, I’ve been in both situations, where a field in a derived type wanted to call an instance method declared on the base class, and in this situation where a base class field initializer has needed to invoke a MustOverride member filled in by a derived type. Both are legal in VB and neither are in C#, and it kinda makes sense. If a C# field initializer could call a base class member that member might depend on fields initialized in the base constructor–which hasn’t run yet–and the results would almost certainly be wrong and there’s no way around it.

But in VB the base constructor has always already run, so you can go wild! In the opposite situation, it’s a little trickier, because calling an Overridable member in a base class initializer (or constructor) could result in accessing fields before they’re “initialized”. But only your implementation knows whether that’s a problem. In my usages that just doesn’t happen. They don’t depend on instance state but they can’t be Shared members because you can’t have a Shared Overridable member in any language for technical reasons beyond the scope of this blog post. Additionally, it’s well-defined what happens to fields before user-written initializers are run, they’re initialized to their default values, like all variables in VB. There are no surprises here.

So why? I don’t actually know that my scenarios were what the original VB.NET team had in mind when they designed this. It just really worked out for me! I think it’s actually a much simpler reason: The VB design ensures that you can always write in a field initializer what you could write in the constructor. We intuitively think of field initializers as shorthand for writing assignments in the constructor. With this design they are very much that.

Incidentally, this difference is very important to keep in mind when designing auto-prop initializers and primary constructors and an example of why you can’t just copy-paste a C# design on top of VB, especially if that design relies on assumptions about VB imposing the same restrictions as C#.

17. The implicitly declared backing field of VB events have a different name than they do in C# and are accessible by name

This matters maybe for Reflection and serialization (which is really just more Reflection) reasons. Given a simple declaration of an event named E, in VB there’s a (hidden in IDE) field named EEvent declared. In C# the field is also named E and the language has special rules about when the expression E refers to the event and when it refers to the field.

18. The implicitly declared backing field of VB auto-implemented properties have a non-mangled name in VB and are accessible by that name

Given an auto-prop named P, a field is generated named _P‘. It’s hidden in IntelliSense but can be accessed if needed. In C# this field has a “mangled” name, meaning it’s a name that cannot be declared or referenced in C# itself, usually including special characters.

Why? The VB team elected to use a well-known name in the case 1) because it’s consistent with the precedent set by event backing fields and ‘WithEvents’ variables, and 2) so that the name could stay the same if the auto-prop is ever expanded into a normal property, which is important for preserving serialization backward compatibility.

19. The implicitly declared backing field of VB read-only auto-implemented properties are writeable

Some people wanted the fields to also be read-only because … purity. But VB has a strong precedent for “escape hatches” to its magic features. Even though the backing field of a WithEvents variable, non-Custom events, and read-write auto-props are almost never intended to be accessed directly, there’s still a hidden way to bypass the accessors if your scenario requires it. The variables are hidden from IntelliSense so you have to go out of your way but if you need the flexibility it’s there. Philosophical self-consistency FTW! Also, it gives VB a concise feature comparable to what in C# would be declared as a private set; auto-prop.

Class Alarm
Private ReadOnly Code As Integer
ReadOnly Property Status As String = "Disarmed"
Sub New(code As Integer)
Me.Code = code
End Sub
Sub Arm()
' I'm motifying the value of this externally read-only property here.
_Status = "Armed"
End Sub
Function Disarm(code As Integer) As Boolean
If code = Me.Code Then
' And here.
_Status = "Disarmed"
Return True
Return False
End If
End Function
End Class

20. Attributes on events sometimes apply to the event backing field

Specifically the NonSerialized attribute.

Because VB didn’t get a syntax for declaring an expanded Custom event until 2005 (?) and doesn’t have an attribute target syntax for type members, it was impossible to explicitly declare the backing field for an event and thus apply the NonSerialized attribute. This is something you’d absolutely want to do because objects listening to your events aren’t really part of “your” state-proper and shouldn’t be part of what’s considered your “data contract”.

This really bit some people hard who would serialize objects because the serializer would attempt to serialize the event backing field, and thus all the listeners to the event. So if, for example, you had a data class that was also two-way data-bindable (and thus declared a PropertyChanged event) the serializer would try to serialize any controls bound to that object, and of course fail.

And example of this near and dear to my heart is from earlier versions of Rocky Lhotka’s CLSA “Expert Business Objects” framework, which would use serialization to provide undo/redo capabilities–it would serialize a copy of what the object looked like before when you changed something and deserialize if you undid the changes–as well as object cloning, and network marshalling. So adding this special-case really unblocked impacted customers. Plus, it’s pretty neat not having to completely re-implement a full event manually just to opt out of serialization.


21. The scope of a label is the body of the entire method containing it; you can jump into blocks (w/ exceptions)

That’s right, you can GoTo a label inside a block from outside of that block. There are some restrictions, usually when doing so would by-pass some important language construct initialization, e.g. you can’t jump into a For or For Each loop; Using, SyncLock, or With block, and I think certain cases involving lambda captures and Finally blocks. But If and Select Case blocks, Do and While loops, and even Try blocks are fair game and I’ve encountered scenarios for all of them:

Module Program
Sub Main()
Dim retryCount = 0
' IO call.
Catch ex As IO.IOException When retryCount < 3
retryCount += 1
GoTo Retry
End Try
End Sub
End Module

The reason for this is very likely the fact that prior to .NET VB didn’t have “block” scoping of anything. In VB6 all the way back to my Quick Basic days, labels (and variables) were scoped to the entire containing method. When I started coding in QB, indentation was a stylistic suggestion. It made the code read better but it wasn’t as much a reflection of the structure of “scopes” and often enough all my code was left-aligned. Also, if you’re going to be using GoTo it’s unlikely that block scoping is your highest order bit, in fact it would probably defeat the purpose.

Heads up: This Try scenario is also important to keep in mind if VB ever supports Await in Catch and Finally blocks since the code generated if there is such a GoTo in the Catch would need to be a little different.

22. Local variable lifetime <> variable scope

As a follow-up to the previous point, in VB a (non-Static) local variable’s lifetime (how long that variable has a value in it) isn’t the same as its scope (where it can be referenced by name). And it makes sense, especially considering the previous point. In my example, execution would leave the Catch block on an exception and retry up to 3 times. Even though any variables inside the Try block are out of scope inside the Catch and can’t be referenced, it’s both reasonable and necessary that upon execution re-entering the Try block that those variables still have their previous values.

Again, prior to VB.NET, variables were scoped to the method so it didn’t really matter. But even aside from that at the CLR level this is true and even in the absence of VBs ability to jump into blocks. It’s also consistent with the debugging experience: if, while debugging, the developer moves the instruction pointer back into a block that had been exited previously.

Technically, C# specifies that the actual lifetime of a variable is implementation-dependent so the behavior under the debugger isn’t “wrong”. It’s just that in VB.NET the actual lifetime is far more observable.

23. Variables are always initialized with the default value for their type

I wasn’t going to call this out at first, but it comes up a lot in language design discussions, because C# has these hardcore set of rules about “definite assignment”. The idea is that the language needs rules to ensure that you never accidentally access “uninitialized memory”. This is actually dangerous, if leftover (or code) in memory from some previous usage is now loaded into a pointer variable which is accidentally dereferenced and now your app crashes or computer blue screens. This is a piece of that C/C++ legacy. Because C is all about that performance, baby! Every operation is precious and any CPU time being spent needs to be explicit. So automatically zero-ing out memory before code uses it for safety purposes is right out. If the user desperately wants to not access garbage data they should write it out explicitly so that it’s explicit that they’re asking for a paying for those CPU cycles and so that, in the event that they’ve written a perfectly optimized algorithm that initializes that variable with some non-zero value anyway they’re not paying for both the zero-init and the explicit init. But yeah, BASIC languages don’t feel that way so all our variables are auto-initialized to their default value and there’s no way to access “random” memory so every variable doesn’t need = Nothing, = 0, = False, etc.

Consequently, VB flow analysis is more like guidelines than actual rules.

Definite assignment rules impose a hefty tax on language design as well because the C# rules have to be kept air-tight to guarantee you never access a variable at a location where it has not been definitely assigned on every path to that location. VB has warnings in some situations like this but they were originally specifically targeted at helping developers find potential sources of null references, not at encouraging more redundant initializers. In Roslyn, however, the APIs still use a more rigorous definition of “definitely assigned” so that the refactoring experience is at its best, though technically variables are always definitely assigned.

24. RaiseEvent does NOT throw an exception if the event backing field is null

I’ve seen this a few times in the past when someone tried to translate some C# into VB. VBs ‘RaiseEvent’ isn’t just a translation of directly invoking the backing field, it actually checks for null (in a thread-safe way) so the null handler scenario isn’t one you ever have to think about.

' You don't have to write this:
If PropertyChangedEvent IsNot Nothing Then
RaiseEvent PropertyChanged(Me, e)
End If
' You don't have to write this:
Dim handlers = PropertyChangedEvent
If handlers IsNot Nothing Then
handlers(Me, e)
End If
' You don't have to write this either:
PropertyChangedEvent?(Me, e)
' Just write this:
RaiseEvent PropertyChanged(Me, e)

Consequently, while using the null-conditional invocation syntax in C# as of VS2015 is a big win for C# in this scenario, it’s a much smaller win for VB (still a win though) and I would not advise anyone start going out of their way to use it needlessly (usually); idiomatic VB.NET code will continue to serve you well.

25. Assignments aren’t always the same; sometimes assigning a reference type makes a shallow clone

Here’s one of those differences that if you haven’t noticed it in the last 17 years, it probably doesn’t matter. When you assign a boxed value type to assignee of type Object, the compiler injects a call to a method called System.Runtime.CompilerServices.RuntimeHelper.GetObjectValue. This is a special method implemented internally inside the CLR. What it does is, given a reference to an object:

  • If the object is a reference type, it returns that reference unchanged.
  • If the object is a boxed value type that’s known to be immutable (e.g. all primitive value types like Integer), it returns that reference unchanged.
  • If the object is any other boxed value type, it copies the value into a new boxed value and returns a reference to that.

The reason this is done is to preserve value type semantics, that is to say that values are always copied, even in late-bound situations. So even if I have a boxed mutable structure and I pass it, still boxed, to a method, and that method performs late-bound operations on the boxed object that change it, it’s still only operating on a copy of the value, not the caller’s copy. So if you’re code is weakly typed and all dynamic, or strongly-typed and early bound, or somewhere in the middle the semantics of value-types stays the same.

I ran into this exactly once in my career. It was a program like this:

Class MyEventArgs
Property Value As Object
End Class
Structure MyStruct
Public X, Y As Integer
End Structure
Module Program
Sub Main()
Dim defaultValue As Object = New MyStruct With {.X = 3, .Y = 5}
Dim e = New MyEventArgs With {.Value = defaultValue}
RaiseEvent DoSomething(Nothing, e)
If e.Value Is defaultValue Then
' No handlers have changed anything.
End If
End Sub
Event DoSomething(sender As Object, e As MyEventArgs)
End Module

There was some sort of event pipeline, similar to a WPF value converter, where the code would start out with a default value and give other code the opportunity to modify that value. If nothing was changed then the code would take a fast path. Logically, if I started out with a boxed value and the event args referenced the same boxed object after raising the event then none of the handlers had updated the value. But I soon learned that that was never the case. I don’t think I was ever able to workaround this behavior so I probably gave up on using a boxed value type and changed my default value to a class.

Incidentally, the documentation for the helper indicates that other “dynamic languages” may also make use of this helper to preserve value semantics. I didn’t check IronRuby/Python, but I did check C#’s dynamic (and the C# compiler) and C# doesn’t inject calls to GetObjectValue on assignment between locations typed as dynamic. But, funny enough my first instinct when checking this was to use object.ReferenceEquals to see if the references were the same and that did copy the boxed value, somewhere, deep in the bowels of C# dynamic callsites (because it was a dynamic call). But when I switched to using object == it didn’t. So C# sometimes at least shares this goal of preserving value semantics in late-bound situations.

26. Select Case does not support fall-through; no break required

In the code below, Friday is the only weekday and Sunday is the only weekend day, and the other 5 days of the week are lost.

Module Program
Sub Main()
Select Case Today.DayOfWeek
Case DayOfWeek.Monday:
Case DayOfWeek.Tuesday:
Case DayOfWeek.Wednesday:
Case DayOfWeek.Thursday:
Case DayOfWeek.Friday:
Case DayOfWeek.Saturday:
Case DayOfWeek.Sunday:
End Select
End Sub
End Module

One day a developer on the Roslyn team from the C# side calls me over, pulls some code up on his laptop, and he says, “You know what I learned today? That doesn’t fall through!” and I say “No, it doesn’t” and there was much laughter. VS actually removes those colons if you type them, but it just so happened that the code was generated and no one reviewed the generated code, it just didn’t work right. We fixed it, though!

So these are different for a classic reason. C# is designed to be familiar to developers from the C-family of languages and that’s how C switches work. They fall through from one case to another. Incidentally, C# doesn’t technically support fall-through either, unless the case section is completely empty. If you put anything in there you either need an explicit goto or a break. There is an equivalent of break in this context in VB, Exit Select but it’s not needed at the end of a block because there’s no fall-through at all in VB.

27. Case blocks each have their own scope

This one was a surprise to me, the first time it came up. But if you were to write the equivalent code to this in C# you’d get an error:

Module Program
Sub Main()
Select Case Today.DayOfWeek
Case DayOfWeek.Monday,
Dim message = "Get to work!"
Case DayOfWeek.Saturday,
Dim message = "Funtime!"
End Select
End Sub
End Module

The error would say that message is already declared and can’t be declared twice, because in C# the entire switch statement is a single scope and each case label is really just that, a label–they don’t declare scopes. Which kinda makes sense (in C at least), if you’re falling through from one section to the next there may be state than needs to be shared between sections, I guess.

28, 29, 30. Select Case works on non-primitive types, can use arbitrary non-constant expressions in tests, and uses the = operator by default

So here’s a thing I just never knew until a language design meeting started to pivot on faulty assumptions about what existing code could exist today using Select Case and how potential new features would need to design around it.

I’ve got a great example of this in mind for a follow-up post but for now I’ll talk about the philosophical reasons these things are different. It boils down to this:

  • Select Case is shorthand for when you want to test the same value multiple times but…
    • don’t want to repeat the value (or its side-effects) multiple times across an If/ElseIf/Else block and/or…
    • don’t want to store the value in a variable in order to perform multiple tests
  • switch is a fast opcode/native instruction known as a “jump- or branch-table

That’s the difference that sums up differences 26-30. switch in the past has constrained itself to scenarios where the performance of the code generated by the compiler is faster than multiple successive if tests. There is an actual IL instruction switch which is much more efficient than multiple If tests and the VB compiler will use it under the circumstances that it is faster, as an optimization. But philosophically the switch in the past has been limited to such scenarios, I believe, as an inheritor of C’s belief in performance transparency. In VB it’s purely about convenience of expressing yourself.

31. Variables declared inside loops retain their value between iterations, sort of

In this example, every iteration x has the same value it ended the previous iteration with so the numbers -1, -2, -3 are written to the console:

Module Program
Sub Main()
For i = 1 To 3
Dim x As Integer
x -= 1
End Sub
End Module

Technically each iteration “a fresh copy is made of all local variables declared in that body, initialized to the previous values of the variables” (per the spec). It’s a subtle difference that only became observable in VB2008 when lambda expressions were introduced and later:

Module Program
Sub Main()
Dim lambdas = New List(Of Action)
For i = 1 To 3
Dim x As Integer
x -= 1
lambdas.Add(Sub() Console.WriteLine(x))
For Each lambda In lambdas
End Sub
End Module

This example also prints out -1, -2, -3. Because technically each x is a “fresh copy”, the lambda expression captures the value of x for that iteration only, which is most often what you want. But you still get to carry the value over from previous iterations as though it were a single variable x for the life of the loop. Try representing that in a flow analysis API – I dare you! (“The… variable… assigned to… itself?”)

Why? I asked around and those who were on the team the longest couldn’t remember exactly, but thinking back on it it makes a lot of since combined with the rationale for #22. It’s the best way to introduce block-level scoping while preserving the behavior of method-level variable lifetime, and once you add lambdas to the mix copying the variable is the only design that is practical and intuitive.

Incidentally, the VB and C# design teams decided to change the behavior of For Each control variables in VS2012(?) so that lambda expressions would capture them ‘per iteration’. This is 10000% more practical and intuitive than what the behavior was before (in fact, VB reported a warning before in this scenario because the behavior was so un-intuitive). Additionally, the VB language design team very carefully considered changing the behavior of For control variables to be like variables inside the loop so that you could still change its value inside the loop but that once captured the current value would freeze like this. That change was considered with the idea that VB For loops were a lot closer to VB For Each loops than C# for and foreach loops were to each other. Ultimately we never made that change but the VB For loop still reports a warning if the control variable is captured because the behavior is often surprising.

32. The three For loop expressions are only ever evaluated once at the beginning of the loop

Once started you can’t change the bounds of a For loop. The expressions in the loop header are only evaluated once and the results cached, so this example will print 1,3,5,7,9 even though changing the upper-bound and increment might make you think it’ll go on forever.

Module Program
Sub Main()
Dim lower = 1,
upper = 9,
increment = 2
For i = lower To upper Step increment
upper += 1
increment -= 1
End Sub
End Module

This example will print 1,3,5,7,9 even though changing the upper-bound and increment might make you think it’ll go on forever.

This is an optimization if those expressions are complex and have enough side-effects–the size of an array never changes after all–but it does mean that if you modify a collection you’re iterating over you won’t iterate over new elements and you might get IndexOutOfRangeExceptions trying to index removed elements.

That said, I’m not sure the world would work without this if you consider that unlike the C-style for loop the condition of the VB For loop is inferred. Have you ever considered how VB knows whether a loop For i = a To b Step c is counting up (and should stop once i > b) or counting down (and should stop once i < b), particularly if c isn’t known at compile-time? It’s all quite exciting reading, especially in the late-bound case, but that house of cards would come tumbling down pretty hard if b were sometimes positive and sometimes negative. I’m not even sure what the expectations should be so thankfully I’ll never have to think about it.

33. For Each loops in VB can use extension method GetEnumerator

In order for be For Each-able a type doesn’t need to implement IEnumerable, the compiler just needs to be able to find a method called GetEnumerator off of the collection being For Each-ed over. For example, I’ve always thought that you should be able to For Each over an IEnumerator in situations where you’ve consumed part of it already and want to resume iteration, e.g.:

Module Program
Sub Main()
Dim list = New List(Of Integer) From {1, 2, 3, 4, 5}
Dim info = list.FirstAndRest()
If info.First IsNot Nothing Then
For Each other In info.Additional
Console.Write(", ")
End If
End Sub
Function FirstAndRest(Of T As Structure)(sequence As IEnumerable(Of T)) As (First As T?, Additional As IEnumerator(Of T))
Dim enumerator = sequence.GetEnumerator()
If enumerator.MoveNext() Then
Return (enumerator.Current, enumerator)
Return (Nothing, enumerator)
End If
End Function
Function GetEnumerator(Of T)(enumerator As IEnumerator(Of T)) As IEnumerator(Of T)
Return enumerator
End Function
End Module

In this example, I’ve taken a queue from my F# friends and split the sequence into the first element and the rest and extended IEnumerator so that I can For Each over any unconsumed items remaining in the sequence.

There’s a general theme in VB that when a language construct needs to find a member with a well-known name that that member can be an extension method. This also applies to, for example, the Add method used by collection initializers. C# by default does not do this but they’ve been getting more relaxed about it (e.g. async/await) with each version. In fact, there was a bug where the Roslyn C# compiler did (accidentally) for collection initializers and they decided to keep it.


34. Boolean conversions

Converting Boolean True to any signed numeric type produces -1, to any unsigned numeric the max value for that numeric, whereas in C# such conversions don’t exist. However, the Convert.ToInt32 method, for example, converts True to 1 and that’s how it’s most often represented in IL. Going in the other direction any non-0 number will convert to True.

Why? The reason VB prefers to use -1 to 1 is because the bitwise negation of 0 (all bits set to 0) in any language is -1 (all bits set to 1) so using this value unifies logical and bitwise operations such as And, Or, and Xor.

Conversions to and from the strings “True” and “False” (case-insensitive of course) are also supported.

35. Conversions between Enum types, and between Enum types and their underlying types are completely unrestricted, even when Option Strict is On

Philosophically the language treats Enum types more as a collection of named constants of their underlying integral type. The place where this is most apparent is equality. It’s always legal to compare any integer to an enum value whereas in C# this gives an error.

Story time: The Roslyn API went through many internal revisions. But in all of them each language had a dedicated enumeration called SyntaxKind which would tell you what kind of syntactic construct a node represented (e.g. IfStatement, TryCastExpression). One day a developer was using an API that attempted to abstract over language and returned one of these SyntaxKind values but only as an Integer and after not getting an error when comparing a raw Integer to SyntaxKind promptly came to my office to complain “That it’s really an int is an implementation detail, I should be forced to cast!”.

Years later during another revision of the API we removed the properties (Property Kind As SyntaxKind) that told you the language specific kind entirely and all the APIs started returning Integers. All the C# code broke and all the VB code continued worked as though nothing happened.

A bit after that we decided to rename that property to RawKind and add extension language-specific methods called Kind(). The all the C# code broke because parentheses were required for method invocations but since they aren’t in VB, once again all the VB code continued working as though nothing happened.

36. Overflow/underflow checking for integer arithmetic is entirely controlled by the compilation environment (project settings) but VB and C# use different defaults; overflow checking is on by default in VB

Integral types each have a range, so for example Byte can represent values 0 to 255. So what happens when you add Byte 1 to Byte 255? If overflow/underflow checking is turned off the value wraps around to 0. If the type is signed it wraps all the way around to the lowest negative number (e.g. -128 for SByte). This likely indicates an error in your program. If overflow/underflow checking is on an exception is thrown. To see what I mean, take a look at this harmless For loop.

Module Program
Sub Main()
For i As Byte = 0 To 255
End Sub
End Module

By default, in VB this loop will throw an exception (because the last iteration of the loop increments beyond the bounds of a Byte. But with overflow checking off it loops indefinitely because after 255 i becomes 0 again.

Underflow is the opposite situation where subtracting below the minimum value for a type wraps around to the max value.

A more common situation for overflow is just adding two numbers. Again, take the numbers 130 and 150, both as Byte. If you add them the answer is 280, which can’t fit into a Byte. But your CPU doesn’t see it that way. Instead it says the answer is 24.

This has nothing to do with conversions, by the way. Adding two bytes produces a byte; it’s just the way binary math works. Though you can also get an overflow by performing a conversion when you try to convert a Long to an Integer for example. Without overflow checking the program just chops off the extra bits and stuffs as many will fit into that variable.

Why the difference? Performance. The CLR checking for overflow costs a tiny bit of computing time versus doing it the unchecked way as do all safety checks. VB comes from a philosophy that developer productivity trumps performance so you’re opted into the safety check by default. The C# design team might make a different decision on project defaults today but if you consider that the first C# developers were converted C/C++ developers that group of people would probably demand that the code not silently do any extra stuff that would cost CPU cycles; it’s a hard philosophical difference.

Trivia: Even if overflow/underflow checking is off, converting a Single or Double value of PositiveInfinity, NegativeInfinity, or NaN to Decimal will throw an exception as none of those values are represent-able in the Decimal type at all.

37. Conversions from floating-point numbers to integral types using bankers rounding rather than truncating

In VB, if you convert the number 1.7 to an integer, the result will be 2. In C#, the result will be 1. I can’t speak to mathematical norms outside of America but my instinct when going from a real number to a whole number is to round. And nobody I’ve ever met outside of a programmer thinks that the closest whole number to 1.7 is 1.

There are actually several ways one can round and the type of rounding VB (and .NET’s Math.Round method) uses by default is called bankers’ or statisticians’ rounding. Which means that for a number halfway between two whole numbers VB rounds to the nearest even number. So 1.5 rounds to 2 and 4.5 rounds to 4. Which actually isn’t how we’re taught to round in school–I was taught the round up from .5 (technically to round away from zero). But as the name suggests, Bankers rounding has the benefit that over a large set of calculations you break even on rounding as opposed to always giving away money or always keeping the extra. In other words, it limits the skewing of data over a large set by limited statistical bias.

Why the difference? Rounding is more intuitive and practical, truncating is faster. If you consider the use of VB in line of business applications and especially in applications like Excel macros running VBA, just willy nilly throwing away the decimal digits all the time would cause… problems.

I think there’s a case to be made that how one converts is truly ambiguous and should always have to be explicit, but if you’re going to pick one…

38. It is not an error to convert NotInheritable classes to and from interfaces they do not implement at compile-time

Generally speaking if you test a class that’s NotInheritable for an interface you can know at compile-time whether such a conversion is possible because you know all the interfaces that type and all of its base types implement. You can’t know that such a conversion isn’t possible if the type is inheritable because the runtime type of the object being referenced might actually be of a more derived type that does implement that interface. However, there is an exception to this due to COM interop where at compile-time it may not appear that a type has any relation to an interface but at runtime it does. For this reason the VB compiler instead reports a warning in such cases.

Why? VB and COM grew up together, back when they were kids in the old neighborhood. So there are several design decisions in the language where VB has a stronger consideration for things that existed only in COM at the time .NET 1.0 shipped.

39. Attempting to unbox a null value to a value type produces the default value of the type rather than a NullReferenceException

I suppose technically that’s also true for reference types, but, yes:

  • CInt(CObj(Nothing)) = 0.

Why? Because CInt(Nothing) = 0 and the language aims to be somewhat consistent whether you typed your variables or not. This applies to any structure, not just the built-in value types. See rationale for #25 for more on this.

40. Unboxing supports primitive conversions

In both VB and C# you can convert a Short into an Integer but what if you try to convert a boxed Short to an Integer? In VB the Short will first be unboxed, then converted to an Integer. In C# an InvalidCastException will be thrown unless you manually unbox the short then convert to int.

This applies to all of the intrinsic conversions, so boxed numerics, conversions between strings and numerics, strings and dates (yes, Decimal and Date are primitive types).

Why? Again, to give consistent behavior whether your program is entirely strongly-typed, or typed as Object, or in the middle of a refactoring from one to the other. See #39 above.

41. There are conversions between String and Char

  • A String converts to a Char representing its first character.
  • A Char converts to a String the only way that could make sense.

Because no one but me remembers the syntax for a char literal in VB (nor should they have to).

42. There are conversions between String and Char array

  • A String converts to a Char array of all its characters.
  • A Char array converts to a String of all its elements.

To be clear, these conversions produce new objects, you don’t get access to the internal structure of a String.

Fun story: I was once found (or maybe it was reported and I investigated) a breaking change between .NET 3.5 and 4.0 because in between those versions the .NET team added the ParamArray modifier to the second parameter of the overload of String.Join that takes a String array. The precise setup for this is lost to time (probably for the best) but I believe the reason was that with the ParamArray modifier it was now acceptable to convert a Char array into a string and pass that as the single element to the param array. Fun stuff.

43 & 44. Conversions from String to numeric and Date types support the language literal syntax (mostly)

  • CInt("&HFF") = 255
  • CInt("1e6") = 1_000_000
  • CDate("#12/31/1999#") = #12/31/1999#

This works with the base prefixes and so makes for a very convenient way to convert hexadecimal (or octal) input into a number: CInt("&H" & input). Sadly, this symmetry is bit-rotting at the time of this writing because the VB runtime hasn’t been updated to support the binary prefix &B or the digit group separator 1_000 but I’m hoping this will be fixed in a future version. Scientific notation works but not type suffixes and date conversions also support standard date formats so the ISO-8601 format JSON uses also works: CDate("2012-03-19T07:22Z") = #3/19/2012 02:22:00 AM#.

Why? Other than being convenient I don’t know why this works. But I have been very tempted to propose also supporting other common formats that are near-ubiquitous on the web today like #FF, U+FF, 0xFF. I imagine it could really grease the wheels for some kinds of applications…

45. There are NO conversions between Char and integral types


After reading about all those extra conversions are you surprised? It is illegal in VB to attempt to convert between Char and Integer:

  • CInt("A"c) does not compile.
  • CChar(1) does not compile.

Why? It’s ambiguous what should happen. Usually VB takes a pragmatic and/or intuitive approach in these situations but given expressions CInt("1"c) I think half of readers would expect the number 1 (the value of the character 1) and half would expect the number 49 (the ASCII/UTF code for the character 1). Rather than pick wrong half the time VB has dedicated conversion functions for converting characters to and from their ASCII/Unicode codes, AscW and ChrW respectively.


46. Nothing <> null

The literal Nothing in VB does not mean “a null value”. It means “the default value of the type it’s being used as” and it just so happens that for reference types the default value is a null reference. The distinction only matters when used in a context where:

  1. Nothing is taking on a value type, and…
  2. It is unintuitive from context that it is doing so.

Let’s run through a few examples that illustrate what this means.

First, while perhaps a little weird I don’t think it’s mind-blowing for most people to learn that this program prints “True”:

Module Program
Sub Main()
Dim i As Integer = 0
If i = Nothing Then
End If
End Sub
End Module

The reason is simple enough, you’re comparing an Integer (0) to its type’s default value (also 0). The problem comes in VB2005/2008 when you introduce nullable value types. Given this example:

Module Program
Sub Main()
Dim i = If(False, 1, Nothing)
End Sub
End Module

It’s understandable how someone might assume the type of i is Integer? (Nullable(Of Integer)). But it isn’t, because Nothing gets its type from its context and the only type in this context comes from the second operand and it is plain non-nullable Integer (Nothing is technically always “typeless”). A different way of looking at this problem is this example:

Module Program
Sub Main()
End Sub
Sub M(i As Integer)
End Sub
Sub M(i As Integer?)
End Sub
End Module

Again there is an understandable intuition at Nothing contributes a “nullable” hint here and that the language will choose the overload that takes the nullable but it doesn’t (it picks the non-nullable one as that is “most specific”). At a minimum, one might assume that like C#’s null the expression Nothing isn’t applicable to Integer at all and that the nullable overload will be chosen by process of elimination but that’s again based on the misunderstanding that Nothing = null (Is null?).

Trivia: In C# 7.1 a new expression was actually added to C# that does map to VBs Nothing: default. If you rewrite all three examples above in C# using default instead of null you get exactly the same behavior.

So what can be done about this? Several proposals have floated around but so far none has risen to the top:

  • Report a warning any time Nothing is converted to a value type other than a null nullable value type value (I said it, what?).
  • Pretty-list Nothing to 0, 0.0, ChrW(0), False, #1/1/0001 12:00:00 AM#, or New T (default value for any structure) anytime its run-time value will be one of these.
  • Add new syntax meaning “Null, no really!” such as Null or Nothing?
  • Add new suffix syntax (?) which wraps a value in a nullable to help type inference, e.g. If(False, 0?, Nothing)
  • Add nullable conversion operators for built-in types to make it easier to give hints to type inference, e.g. If(False, CInt?(0), Nothing)

Would love to hear your thoughts in the comments and/or on Twitter.

So let’s recap:

  • The beforetime – VB6 and VBA have Nothing, Null, Empty, and Missing each meaning different things.
  • 2002 – VB.NET only has Nothing (meaning default value in context) and C# has only null (meaning… null).
  • 2005 – C# adds default(T) (meaning default value of type T) because newly added generics create a situation where you need to initialize a value but don’t know if it’s a reference type or a value type; VB does noth… doesn’t do anything because it already has this scenario covered by Nothing.
  • 2017 – C# adds default (meaning default value in context) because there are plentiful scenarios where specifying T is redundant or impossible; VB continues to resist urge to a true Null expression (or equivalent) because:
    • The syntax would be a breaking change.
    • The syntax would not be a breaking change but would contextually mean different things.
    • The syntax would be to subtle (e.g. Nothing?); imagine having to talk about both Nothing and Nothing? out loud to explain something to a person.
    • The syntax might be too ugly (e.g. Nothing?).
    • The scenario of expression a null value is already covered by Nothing and this feature would be entirely redundant most of the time.
    • All documentation everywhere and all guidance would have to be updated to recommend using new syntax mostly deprecating Nothing in most existing scenarios.
    • Nothing and Null would still behave the same at runtime with regard to late-binding, conversions, etc.
    • This might be bringing a cannon to a knife fight.

So there you have it.

Off-topic (but related)

Here’s an example very similar to the second one above but without type inference:

Module Program
Sub Main()
Dim i As Integer? = If(False, 1, Nothing)
End Sub
End Module

This program prints 0 to the screen. It behaves exactly the same as the second example for the same reason but illustrates a separate though related problem. It’s intuitive that Dim i As Integer? = If(False, 1, Nothing) behave the same as Dim i As Integer? : If False Then i = 1 Else i = Nothing. In this case it doesn’t because the conditional expression (If) doesn’t “flow through” target type information to its operands. It turns out this breaks all expressions in VB which rely on what’s called target (contextual) typing: Nothing, AddressOf, array literals, lambda expressions, and interpolated strings with problems ranging from not compiling at all to silently producing the wrong values to loudly throwing exceptions. Here’s an example of the not compiling case:

Module Program
Sub Main()
Dim i As Integer? = If(False, 1, Nothing)
Dim operation As Func(Of Integer, Integer, Integer) =
AddressOf Add,
AddressOf Subtract)
End Sub
Function Add(left As Integer, right As Integer) As Integer
Return left + right
End Function
Function Subtract(left As Integer, right As Integer) As Integer
Return left right
End Function
End Module

This program won’t compile. Instead it reports an error on the If expression that it can’t infer the type of the expression when clearly both AddressOf expressions are intended to produce Func(Of Integer, Integer, Integer) delegates.

What’s important to keep in mind here is that solving the problems of Nothing not always meaning null (unintuitive), Nothing not hinting at nullability (unintuitive), and If(,,) not providing the context for Nothing (and other expressions) to behave intuitively (unintuitive) are all distinct problems and solving one will NOT solve the others.

47. Parentheses affect more than just parsing precedence; they re-classify variables as values

This program prints “3” to the Console:

Module Program
Sub Main()
Dim i As Integer = 3
End Sub
Sub M(ByRef variable As Integer)
variable = -variable
End Sub
End Module

The analogous program in C# would print “-3”. The reason is that in VB parenthesizing a variable makes it behave like a value–a process known as reclassification. At that point the program behaves as if you’d written M(3) rather than M(i) and no reference to the variable i is passed so it can’t be mutated. In C# parenthesizing the expression (for whatever reason) won’t change it from being a variable to a value so calling M will mutate the original variable.

Why? This has always been the behavior in VB. In fact, I just opened my copy of Quick Basic (Copyright 1985) and it’s also the behavior there. Given that pass-by-reference was the default passing convention until 2002 everything else makes perfect sense.

Trivia #1: “How the subroutine got its parenthesis” c/o Paul Vick, Visual Basic .NET Architect Emeritus

Trivia #2: When we were designing the bound tree in the Roslyn compilers, the data structure that represents the semantics of a program (what things mean) rather than the syntax (how things are written), this was a sticking point for the compiler team: whether parenthesized expressions would be represented in the bound tree. In C# parentheses are almost purely a syntactic construct used to control the precedence of how things are parsed ((a + b) * c or a + (b * c)) so much so that the original C# compiler written in C++ threw the fact that an expression had been parenthesized away along with things like white-space and comments. There were several attempts at being consistent between the languages “Can we squint and get rid of them in VB?” or “Can we just live with them in C#?” and ultimately the result per is that BoundParenthesized is a thing in the VB compiler and is not a thing in the C# compiler. In other words, the languages are different here, and we just had to accept that.

48. Me is always classified as a value—even in structures

You cannot assign to Me in VB.NET. Normally this isn’t surprising but maybe one might think that because structures are just a set of values that it would be legit to assign to Me inside of an instance method or constructor of a Structure type as a shorthand for copying but it’s still illegal and passing Me by-reference will simply pass a copy. In C# it is legal to assign to this inside a struct. and you can pass this by-reference inside struct instance methods.

49. Extension methods can be accessed by simple name

In VB, if an extension method is defined for a type and in scope in that types definition you may call that extension method unqualified inside the definition of the type:

Class C
Sub M()
End Sub
End Class
Module Program
Sub Main()
End Sub
Sub Extension(c As C)
End Sub
End Module

In C#, extension methods are only looked for with an explicit receiver (meaning something.Extension). So while the exact translation of the above wouldn’t compile in C# you can access extensions on the current instance by explicitly stating this.Extension().

Why? There’s an argument one could make that normal instance members can be accessed without explicitly qualifying them with Me. and that since extension members act like instance members everywhere else it is intuitive that they behave consistently in this context as well. VB.NET adheres to this argument. Presumably there are other arguments and other languages are free to adhere to them.

50. Static imports will not merge method groups

VB has always supported “static imports” (A Java term combining a C# modifier with the VB statement). It’s what allows me to say Imports System.Console at the top of a file and use WriteLine() unqualified throughout the rest of the file. In 2015, C# also added this capability. However, in VB if import to 2 types with Shared members that have the same name, e.g. System.Console and System.Diagnostics.Debug which both have WriteLine methods it’s always ambiguous what WriteLine refers to. C# will merge the method groups and perform overload resolution and if there’s one unambiguous result that’s what it means.

Why? I think there’s an argument to be made that VB could be smarter here like C# (especially given the next difference). But, there’s also an argument to be made that if two methods come from two different places and have no relationship with each other at all (one is not an extension method on the type defining the other) that it’s… misleading to suggest they’re all options under the same name.

Moreover, there are multiple cases in VB where this same scenario comes up where VB picks the safer route and reports ambiguity, e.g. two methods with the same name from unrelated interfaces, two methods with the same name from different modules, two methods from different levels of the inheritance hierarchy where one isn’t explicitly an overload of the other (difference #6). VB is philosophically self-consistent here. Also, VB made all of those decisions in 2002.

51 & 52. Partial-name qualification & Smart-name resolution

There are a couple of ways to think about namespaces:

  • In one way of thinking, namespaces are all siblings in a flat list and only contain types (not other namespaces). So System and System.Windows.Forms are siblings that share a common prefix by convention but System does not contain System.Windows and System.Windows does not contain System.Windows.Forms.
  • In another way of thinking, namespaces are like folders organized into a hierarchy and can contain other namespaces and types. So System contains Windows and Windows contains Forms.

The first model is particularly useful for displaying namespaces in a GUI without deep nesting. However, my intuition has always been the second. And with regard to Imports statements VB follows the second model and C# using directives behave like the first.

Consequently in VB, if I have imported the System namespace, I can access any namespace inside System without qualifying it with System. To me this is like specifying a relative path. So, if any of my examples where I qualify the ExtensionAttribute I write <Runtime.CompilerServices.Extension> instead of <System.Runtime.CompilerServices.Extension>.

In C#, this is not the case. using System does not bring System.Threading into scope under the simple name Threading.

But it gets even better, because C# does allow for this “relative path”-style partial qualification scenario specifically in the case where the code is defined in that namespace. That is to say, if you’re declaring a type inside System, within that type you may refer to the System.Threading namespace as Threading. And that’s self-consistent because you could write out a namespace declaration and a type declaration both lexically contained inside another namespace declaration and it would be weird if name lookup from the type wouldn’t find the sibling.

But it gets even worse, because though both VB and C# require that namespaces always be fully qualified within the file-level Imports statements/using directives, C# allows you to have a using directive inside a namespace declaration affecting code inside that declaration in that file and within those using directives namespaces can be specified using their simple name.

Enter Quantum Namespaces (not the official name)

But wait, there’s more! The VB model is convenient, but that convenience comes with a risk. Because what happens when System contains a ComponentModel namespace and System.Windows.Forms contains a ComponentModel namespace? It’s ambiguous what ComponentModel means. And what would happen sometimes is that you’d write all this code that just said ComponentModel.PropertyChangedEventArgs and the world would be fine (I vaguely recall that earlier versions of the designers would do this in generated code). But then you’d import System.Windows.Forms (or maybe just reference an assembly which declares a sub-namespace in one you have imported with that name and all your code would break with ambiguity errors.

So in VB2015 we added Smart Name Resolution whereby when you have System and System.Windows.Forms imported and you write ComponentModel. a
Schrödinger-style quantum superposition is created of both the realities where you’re referring to System.ComponentModel and where you’re refering to System.Windows.Forms.ComponentModel until you type another identifier, and if that identifier identifies a child namespace in both realities the wave continues, until the . after which the identifier unambiguously refers to a type that only exist in one temporal universe at which point the entire wave collapses and the cat was always dead. i.e. ComponentModel.PropertyChangedEventArgs must mean
System.ComponentModel.PropertyChangedEventArgs because System.Windows.Forms.
does not exist. This avoids many of the ambiguities that would occur from simply importing a new namespace.

But it doesn’t solve the problem of adding a reference which brings a new top-level namespace Windows into scope because top-level namespaces (absolute paths) always beat out partially qualified ones (relative paths) for various reasons (including performance). So using WinForms/WPF and UWP all in one project may still be painful.

53. Collection initializer Add methods may be extension methods

As mentioned in #33, VB generally includes extension methods when looking for things. The scenario for why you’d want this is when you want to use that concise initializer syntax for collections of complex objects, e.g.:

Class Contact
Property Name As String
Property FavoriteFood As String
End Class
Module Program
Sub Main()
Dim contacts = New List(Of Contact) From {
{"Leo", "Chocolate"},
{"Donnie", "Bananas"},
{"Raph", "The Blood of his Enemies"},
{"Mikey", "Pizza"}
End Sub
Sub Add(collection As ICollection(Of Contact), name As String, favoriteFood As String)
collection.Add(New Contact With {.Name = name, .FavoriteFood = favoriteFood})
End Sub
End Module

C# originally didn’t originally consider extension methods in this context but when we re-implemented collections initializers in the Roslyn C# compiler they did consider them. It was a bug that we decided never to fix (but not a feature that we decided to add) so this is only a difference prior to VS2015.

54. Array creation uses upper-bound rather than size

Surprised this rarely comes up, but when initializing an array in VB with the syntax Dim buffer(expression) As Byte or Dim buffer = New Byte(expression) {} the size of the array is always expression + 1.

This has always been true in Microsoft BASIC languages as far back as the DIM (it means dimension) statement has been in existence. Which, I suppose explains why it works that way, the dimension of the array is from 0 To expression. In past versions of Microsoft BASIC languages you could change the default lower-bound of arrays to be 1 (and could explicitly declare the array with arbitrary lower-bounds like 1984) in which case the upper-bound was also the length (I typically did this) but this ability was lost in 2002.

But on an even deeper level, I’ve heard tell of a language design fad way back then of making declaration syntax model usage syntax that explains why arrays are declared with their upper-bound in VB, why array bounds are specified on the variable rather than the type in BASIC and in C, the pointer syntax in C, why types are on the left in C-derived languages. Think about it, all usages of buffer(10) will use a value from 0 to 10, not 9!

55. VB array literals are f-ing magic not the same as C# implicitly typed array creation expressions

Though these two features are often used in the same scenarios they’re not the same. The main difference being that VB array literals are naturally typeless (like lambdas) and get their type from their context, and in the absence of a type context from their element expressions. The spec illustrates this well:

  • CType({1, 2, 3}, Short()) does not mean CType(New Integer() {1, 2, 3}, Short()) because it is impossible to convert an Integer array into a Short array.
  • CType({1, 2, 3}, Short()) reclassifies the array literal to mean New Short() {1, 2, 3}. There is no spoon.

This is actually pretty cool because it means things can happen with a VB array literal that can’t with a C# implicitly typed array creation. For example, passing an empty one:

  • Dim empty As Integer() = {}

Creating an array of typeless expressions:

  • Dim array As Predicate(Of Char)() = {AddressOf Char.IsUpper, AddressOf Char.IsLower, AddressOf Char.IsWhitespace}

Performing per element conversions (intrinsic or user-defined):

  • Dim byteOrderMark As Byte() = {&HEF, &HBB, &HBF} ' No byte literals neeed.

And because target-type isn’t only inferred from array types but also IList(Of T), IReadOnlyList(Of T), ICollection(Of T), IReadOnlyCollection(Of T), or IEnumerable(Of T) you can very concisely pass a variable number of arguments to a method that takes one of those types, rendering ParamArray IEnumerable unnecessary.

Why? Prior to writing this doc I thought this difference was mostly just VB going the extra-mile but I now believe it’s something much simpler. Prior to the introduction of local type inference in 2008 both VB and C# allowed you to initialize an array declaration with the {} “set notation” syntax but you couldn’t use that syntax anywhere else in the language (except attributes, I think). What we think of now as array literals is really just a generalization of what that syntax could do to any expression context + a few other niceties like inferring from the above generic interfaces. Which is really elegant.

56. Anonymous type fields can be mutable AND are mutable by default

This doesn’t impact the anonymous type fields created implicitly by LINQ but the ones you explicitly create may be mutable or not, up to you.

Details on why and how here.

57. Neither CType nor DirectCast are exactly C# casting

There is no exact match between casting/conversion operators between VB and C#.

VB CType

  • Supports user-defined conversions
  • Supports reference conversions (base class to derived)
  • Supports intrinsic conversions, e.g. Long to Integer (see Conversions section)
  • Unboxes complex value types directly
  • Does NOT unbox primitive types directly
  • Does NOT support dynamic conversions (use CTypeDynamic function)

VB DirectCast

  • Does NOT support user-defined conversions
  • Supports reference conversions
  • Does NOT support intrinsic conversions (cannot convert Integer to Byte)
  • Unboxes complex value types directly
  • Unboxes primitive types directly (hence the name)
  • Does NOT support dynamic conversions

C# casting – (Type)expression

  • Supports user-defined conversions
  • Supports reference conversions
  • Supports intrinsic conversions
  • Unboxes complex value types directly
  • Unboxes primitive types directly
  • Supports dynamic conversions

Between the two of them CType is the closest to C# casting in that it can be used in the broadest set of scenarios. In fact, from the perspective of the language it is the conversion operator. But VB and C# allow and prohibit different conversions, have different semantics for the same conversions, and in some cases generate different IL for those conversions. So there’s no way to get exactly the C# set of conversions no more, no less, with exactly the same semantics, and exactly the same code generated with a single operator all the time, nor should there ever be.

In reality, everyone can use CType except for dynamic conversions (conversions which look for a user-defined conversion operator at runtime). CType supports every scenario DirectCast supports and more and in every case where they can both be used they will generate the same IL, with one exception: when converting from Object (or ValueType) to a primitive type, instead of emitting a CLR “unbox” instruction the compiler emits a call to a VB runtime function which will succeed if the type of the object is the target type OR if the value of the object can be converted to that type, e.g. widening a Short to Integer; meaning it will succeed more often than it would in C#. But this only supports intrinsic conversions between primitive types. In an extremely narrow set of scenarios this could matter but for the vast majority of cases it won’t.

Why? The languages support different conversions. The conversion operators should support the conversions the languages support not the conversions other languages support for no particular reason.

58. The precedence of certain “equivalent” operators is not exactly the same

See the specifications for the full tables of operator precedence but they are not the same between the languages, so 5 Mod 2 * 3 evaluates to 5 in VB, the “equivalent” expression in C#, 5 % 2 * 3 evaluates to 3.

Operator precedence is probably the oldest part of either language family. I only noticed this when I considered the impact of operators which only exist in one language (e.g. integer division (\) in VB) on operators after it which might otherwise be at the same level, but the differences appear to be far more pervasive. You have been warned!

59. String concatenation is not the same + and & are not the same with regard to String concatenation and + in VB <> + in C#

Let’s just talk about how VB + (addition) and & (concatenation) differ from each other and from C# +.

Between String and primitive types:


  • “1” + 1 = 2.0
  • “1” & 1 = “11”


  • "1" + 1 == "11"

Between string and types that don’t overload + or &


  • “obj: “ + AppDomain.CurrentDomain ‘ Error: + not defined for String and AppDomain.
  • ”obj: “ & AppDomain.CurrentDomain ‘ Error: & not defined for String and AppDomain.
  • ”obj: “ + CObj(AppDomain.CurrentDomain) ‘ Exception, no + operator found.
  • ”obj: “ & CObj(AppDomain.CurrentDomain) ‘ Exception, no & operator found.


  • "obj: " + AppDomain.CurrentDomain == "obj: " + AppDomain.CurrentDomain.ToString()
  • "obj: " + (object)AppDomain.CurrentDomain == "obj: " + AppDomain.CurrentDomain.ToString()
  • "obj: " + (dynamic)AppDomain.CurrentDomain == "obj: " + AppDomain.CurrentDomain.ToString()

Between numeric types


  • 1 + 1 = 2
  • 1 & 1 = “11”


  • 1 + 1 == 2

Between String and Enum types


  • “Today: ” + DayOfWeek.Monday ‘ Exception: String "Today: " cannot be converted to Double.
  • “Today: ” & DayOfWeek.Monday = “Today: 1”
  • “Today: ” & DayOfWeek.Monday.ToString() = “Today: Monday”


  • "Today: " + DayOfWeek.Monday == "Today: Monday"

Pet peeve: I really dislike that + is even allowed for string concatenation in VB. It’s there for legacy purposes, + always concatenated strings but its current behavior is more like a bug than anything. Why? Because:

  • “10” - "1” = 9.0,
  • “5” * “5” = 25.0,
  • “1” << “3” = 8, and
  • “1” + 1 = 2.0, but
  • “1” + “1” = “11”

Every other arithmetic operator converts the strings to numbers. + being inconsistent is a design bug.

In summary, don’t use + because it looks like the way it’s done in other languages. To get the behavior you intend use & because this dedicated operator exists to unambiguously specify that intent (concatenation, rather than addition). Also, watch out when concatenating enum values, they behave just like their numeric values in that context.

60. Division works sanely: 3 / 2 = 1.5

From time to time I conduct an experiment, I walk up to a random person and ask them “What is three divided by two?”. Most people say 1.5 or one and a half. Only the most indoctrinated among us squint at me and say “That depends. What are the types of the three and the two?”

All of the difference between VB and C# is summed up in this.

Ok, maybe not all of it. That would be pretty mean of me to tell you 60 essays in. Also, I really want you to read the rest. If you really want the C-style behavior, which I suppose is the answer to the question “How many times does 5 go into 9 whole?”, use the integer division operator \. I suppose another justification is that division is closed under the set of integers, excepting division by 0 (which would be relevant if an INumeric interface ever showed up).

61. ^ isn’t exactly Math.Pow

That is to say, it’s not just an alias for Math.Pow. It’s an overload-able operator that has to be explicitly be opted into outside of primitive types. It saddens me how often a custom numeric type doesn’t support it (looking at you System.Numerics.BigInteger).

Trivia: F# also has an overload-able exponentiation operator, **, but when overloading the operator VB and F# emit different names, op_Exponent and op_Exponentiation respectively. Though F# actually looks for a method called Pow on the operand types. Meaning these languages don’t inter-operate well with each other. A sad fact I’d like to see fixed one day.

62. Operator =/<> are never reference equality/inequality

Sometimes in C# == will use (overloaded) operator equality, sometimes language equality, and sometimes reference equality (if the operand types don’t overload equality, are object, or are interface types). In VB this operator will never mean reference equality; VB has separate operators (Is/IsNot) for reference equality.

Story time: At some point in the history of Roslyn we had a class hierarchy that overloaded value equality. Actually, we had two such hierarchies. One day we decided to abstract over both with an interface hierarchy. All of the VB code correctly broke when updated to use the interfaces because = stopped being valid, but there was a bug tail on the C# side because much code that was previously using overloaded value equality silently started using the much stricter requirement of reference equality.

63. Operator =/<> on strings are not the same (or any relational operators for that matter)

String equality in VB is different in a few ways.

First, whether string comparisons use a binary comparison (and thus are case-sensitive) or a culture-aware (and thus are case-Insensitive) is governed by whether Option Compare Binary or Option Compare Text is specified at the file level and/or what its setting is at the project. The default for all projects in VS is Option Compare Binary, btw.

This setting governs all explicit and implicit String comparisons (but not Char comparisons) that occur in the language but not most API calls. So:

  • Equality/Inequality: “A” = “a” / “A” <> “a”
  • Relation: “A” > “a”
  • Select Case statements: Select Case “A” : Case “a”

But not:

  • Calls to Equals: “A”.Equals(“a”)
  • Calls to Contains: ”A”.Contains(“a”)
  • The Distinct query operator: From s In {“A”, “a”} Distinct

But there’s a second far more impactful difference that may surprise you: in the VB language, null strings and empty strings are considered equal. So, regardless of the setting of Option Compare, this program will print “Empty”.

Module Program
Sub Main()
Dim s As String = Nothing
If s = "" Then
End If
End Sub
End Module

So technically s = “” is VB shorthand for String.IsNullOrEmpty(s)

Practically speaking the distinction doesn’t trip people up as often as you might think since the operations one can do on a null string and an empty one are almost exactly the same. You never invoke a member of an empty string because you know what all the answers will be and concatenation treats null strings as empty.

Why? I think of Option Compare Text as more a back-compat option but I get why it existed in the first place. There are a lot of contexts where you want string comparisons to be case-insensitive.

In fact, most contexts where I use strings I want them case-insensitive.

Essentially all contexts outside of passwords and encryption keys. But everywhere else I don’t want my typing a literal lazily to affect my results. Yes, I’m that monster who uses case-insensitive collation on SQL Server because I value my productivity. And if you consider that VB’s history includes not only VB6 but VBA for Office products like Excel and Access and VBScript for Windows and that one web browser that one time, … ain’t nobody got time for case-sensitivity. That said, I accept that .NET is a generally case-sensitive API and I don’t use Option Compare Text because it only affects the language. If there was an setting that affected all the .NET APIs too I’d flip that sucker on and never look back.

As for null being treated as empty, I have a theory. VB6 didn’t have null strings. The default value for String was "". So VB and its runtime philosophically treat the default value for String to be an empty string. In fact, that’s one of the main advantages of using the VB Strings functions like Left and Mid over the methods on String. The runtime functions also treat null like empty. So Len(CStr(Nothing)) = 0 and Left(CStr(Nothing), 5) = “” while CStr(Nothing).Length or CStr(Nothing).Trim() would just blow up.

Fortunately, now you can sort of get this same productivity with the ?. operator (at least the not throwing part).

Why it matters:

So the top-of-mind issue for me with this is that this difference happens everywhere a two string values are compared in the language, so any string comparison in any expression. Including query expressions! The way VB string comparison is implemented is that every time you type “String A” = “String B” that turns into a call to Microsoft.VisualBasic.CompilerServices.Operators.CompareString and when a string comparison in a query expression or lambda expression is converted to an expression tree it shows up in the tree not as an equality comparison but as a call to that function. And invariably every new LINQ query provider throws an exception when it encounters this node. They just don’t expect the pattern because their libraries weren’t tested with VB (or well enough). Which usually means support of that library is delayed until someone can explain to them how to recognize the pattern. This happened with LINQ-to-SQL, LINQ-to-Entities, and a few other ones I encountered during my time at Microsoft. Everything seems fine until a VB developer compares two strings them BOOM!

So, aside from the semantics of string comparison being slightly different than C# it causes a real problem for VB customers using LINQ on new providers. The options to fix it are to 1) change way VB generates expression trees to outright lie or 2) change the way VB generates equal to use a pattern more easily recognizable by LINQ providers. I’m in favor of the latter, though it does require servicing the VB runtime (probably).   

Trivia: Note that I said most API calls. Because Option Compare does actually affect calls to VB runtime string functions such as InStr, Replace, and other members of the Microsoft.VisualBasic.Strings module. How does a compilation setting affect the operation of an already compiled library function, you ask?

Well, you know the way the compiler can pass the current filename or line number in as the value for some optional parameters if they’re properly decorated? Turns out before that feature was added the same scheme was used for the Strings functions: The compiler passes in a value indicating the setting at that point in the program to an optional parameter.

64. Nullable value types use three-valued logic (null propagation in relational operators)

VB and C# handle nullable differently. Specifically, in the area of null-propagation.

If you work a lot in SQL you’re likely very familiar with null-propagation. In short, it’s the idea that given some operator (e.g. +), that if one or more of its operands is null the result of the entire operation is also null. This is similar to the ?. operator where given the expression obj?.Property if obj is null the entire expression results in a null value, rather than throwing an exception.

When dealing with various unary and binary operators and nullable value types, VB and C# both propagate nulls. But, they differ in when they do in a key area: relational operators.

In VB, specifically when dealing with nullable value types, if either operand is null the entire expression is null, with two exceptions. So, 1 + null is null and null + null is null. But this doesn’t just apply to arithmetic operations, it also applies to the relational operators (e.g. = and <>) and this is where C# differ:

  • All of the VB relational operators other than Is/IsNot return Boolean?
  • All of the C# relational operators (==, !=, >, <, >=, <=) return bool instead of bool?

In VB (again specific to nullable value type arguments), comparing a null value with any other value results in null. That is to say, instead of = returning its usual Boolean result, it returns a nullable Boolean? which may be True, False, or null. This is known as three-valued logic. In C# the result of the comparison is always a non-nullable bool value which is fittingly known as two-valued logic.

Note that I said any value. That includes null itself. So NULL = NULL is NULL, not TRUE, in VB.

So a couple of fun consequences to the respective designs:


That broke my mind. Null is not greater than itself, but is equal to itself, and yet not greater than or equal to itself in C#.

And that’s the crux of the issue. If C# used the VB model, the most natural way to ask the question “Is this value null?” in C# (if (value == null)) would fail every time. Here’s a 2004 post saying as much. VB doesn’t have this problem because VB has separate operators for value equality (=/<>) and reference equality (Is/IsNot) so the idiomatic way to check for null in VB is Is Nothing returns a regular non-nullable Boolean.

Earlier, I mentioned an exception to the rule that if either operand is null the entire expression is null in VB. That exception is with the And/AndAlso and Or/OrElse operators.

When the operands are Integer? type (and other integrals), both VB and C# propagate nulls as you would expect:

  • 1 AND NULL is NULL
  • 1 OR NULL is NULL

When the operands are Boolean? type, in VB it’s more complicated.


In other words, if the True/False result can be definitively computed based on knowing one operand the result will be that value, even if the other operand is null. This also means short-circuiting logic operators AndAlso and OrElse work as expected.

In C# it’s not legal to apply either the short-circuiting (&&/||) or non-short-circuiting (&/|) logic operators to nullable boolean (bool?) operands. Which, isn’t as problematic as I first thought because given that all the relational operators produce non-nullable booleans it’s fairly uncommon for a nullable boolean operand to sneak into an expression anyway.

Why does it matter?

Usually the VB behavior is only surprising when someone writes code like this:

Imports System.ComponentModel
Class BindableRange
Implements INotifyPropertyChanged
Private _EndDate As Date?
Property EndDate As Date?
Return _EndDate
End Get
Set(value As Date?)
' This line here:
If value = _EndDate Then Return
_EndDate = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(EndDate)))
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
End Class
Module Program
WithEvents Range As New BindableRange
Sub Main()
Range.EndDate = Today
Range.EndDate = Today
Range.EndDate = Nothing
Range.EndDate = Nothing
End Sub
Private Sub BindableRange_PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
Handles Range.PropertyChanged
Console.WriteLine(e.PropertyName & " changed.")
End Sub
End Module

You might be surprised to learn that this program prints “EndDate changed” thrice three times instead of two. Remember when I said that in VB null does not equal itself? Because it never equals itself when the EndDate property set checks to see if the new value is the same as the old value the check fails the second time the code assigns Nothing to the property.

This is usually when the VB developer says, “Ok, I see how this works. I’ll invert it”:

If value <> _EndDate Then
    _EndDate = value
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(EndDate)))
End If

But that doesn’t work either! In fact the event will never raise in the code now. Rather, it will only raise if the value changes between two non-nullable values but not been a value and a null or vice versa. Because null is neither equal nor unequal to itself. The way to correctly fix the bug is to write this:

If value Is Nothing AndAlso _EndDate Is Nothing Then Return
If value <> _EndDate Then Return

Why is it different/Which is better?

I mentioned why the C# team decided to go with their design and that those concerns do not generally apply to VB at all. When thinking about null values (either with nullable value types or reference types) I often see a conflict between two notions of what null represents.

One way to look at null is that it’s a value that means “none” or “does not exist”. Usually this is what it means with reference types.

But in other cases null means “unspecified” or “unknown”. This is often what is meant by optional parameters which weren’t provided; not that the caller intended you use no comparer but that they were fine with you using the default comparer. And if you look at the Roslyn code base there is actually a type called `Optional(Of T) which is used to describe this notion for reference types, because it’s otherwise impossible to distinguish between values that are intended to be null and values which simply haven’t been provided.

And if you use the latter interpretation, NULL as “an unknown value”, all of the three-valued logic in VB makes sense:

  • If I ask you “Is three greater than an unknown value?” you can only answer “I don’t know”.
  • Likewise “I have two boxes containing unknown items, are they the same item?” “I don’t know”.

And that’s probably why this is also the interpretation that SQL databases use by default. By default, if you try to compare NULL in SQL with any value the answer you get is NULL. And that makes sense dealing with data especially. Everyone reading this post right now would have a NULL in the DateOfDeath column. That’s not the same as us all dying on the same day. If several people fill out a form, most people will not fill out their middle names though they may (it’s optional). This does not mean that all of those people have the same middle name though some people legitimately have no middle name and you could argue that the value of the middle name is the empty string but you get how the meaning of NULL is open to interpretation particularly in SQL databases (with exceptions).

Which brings us back to VB. What’s the killer scenario for nullable value-types in 2008 when full generalized support for nullable value types were added to VB?


The VB model provides consistency between the database where these nullable value types are likely coming from and the language and between comparisons as they appear in a LINQ query and as they run on the server. That’s super compelling to me!

But there’s a catch. In SQL Server at least, there’s an option SET ANSI_NULLS OFF which causes SQL expressions to behave more like C# so you can write WHERE Column = NULL. And I’ll admit, I usually set this to OFF in the past (along with making my database collation case-insensitive). So, I reached out to the SQL Server team (years ago) for guidance. I asked, “What’s the deal with this option? I use it. Is it the way to go and should we add something like Option ANSI_NULLS Off to VB.NET?”. Their response is basically summed up on the docs for the option:

In short, that option is a back-compat thing, will quite possibly go away in the future, and they’d like all humans using SQL Server to adapt to the current VB way of thinking.

So there you have it!

65. Overloaded operators don’t always have a 1:1 mapping

There are cases where VB supports two notions of an operator that other languages would unify, e.g. regular vs integral division. In those cases, overloading the operator in VB may silently overload other operators to be usable from other languages.

Likewise, there are cases where other languages overload certain operators separately, e.g. logical and bitwise negation or signed and unsigned bitshifting. In those cases, VB may recognize such overloads defined in other languages if they are the only flavor available, and in cases where both flavors are available VB may ignore one flavor entirely.

Section 9.8.4 of the spec is the definitive list on these mappings.

66. Function() a = b is not the same as () => a = b

I’ve seen this a few times in translated code. It’s easy to get into the habit of thinking C#’s () => expression syntax always maps to a Function() expression syntax in VB. However, the Function() lambda is only for expressions–lambdas which return something–which assignment is not in VB. Using this syntax with a body of the form a = b will always produce a delegate which compares a and b (returning a Boolean) rather than assigning b to a. However, because of VB delegate relaxation this lambda can still safely (and silently) be passed to a Sub delegate (on that does not return a value). In these cases the code just silently does nothing. The correct translation of () => a = b from C# to VB is Sub() a = b. This code is a statement lambda which correctly contains an assignment statement and can be used for its side-effects.

It has always been the case that whether the = operator denotes a comparison or an assignment is determined by the context; in a statement context (such as a Sub lambda) it denotes assignment, in expression contexts(such as a Function lambda) it denotes comparison.

67. An Async Function lambda will never be interpreted as an async void lambda

In C# it is syntactically ambiguous when writing an async lambda expression whose body does not return a value whether that lambda is intended to be a Task-returning async lambda, or a void-returning async lambda. There’s a rule in C# overload resolution to prefer the Task-returning interpretation if available.

VB.NET does not have this syntactic ambiguity as void-returning Async lambdas use the Async Sub syntax and Task-returning and Task(Of T)-returning lambdas use the Async Function syntax. That said, there’s a different situation that can occur in VB which is relaxing a Task-returning Async lambda into a void-returning delegate type by dropping its return value. This lambda does not behave like an Async Sub and a warning was added in the event that this relaxation ever occurs.

68. Queries are real(er) in VB

To illustrate my sensational title, look at this example in VB:

Class Foo
'Function [Select](Of T)(selector As Func(Of String, T)) As Foo
' Return Me
'End Function
Function Where(predicate As Func(Of String, Boolean)) As Integer
Return 0
End Function
End Class
Module Program
Sub Main()
Dim c As New Foo
Dim q = From x In c Where True Select x
End Sub
End Module

It will not compile in VB for two reasons but will in C#. First, the type Foo doesn’t have a Select method and so is not queryable, so it’s not even legal to use a Where operator on it. But were you to un-comment the definition of Select to resolve that error the final Select operator won’t compile now because Integer isn’t queryable. In C# however the translation is specified syntactically such that the entire query reduces to a simple call to .Where (the final select is elided). Because it doesn’t manifest all of the query clauses written it doesn’t give errors when the pattern is malformed.

This is a difference that only comes up in language design, or when trying to represent LINQ in an API. But the way that queries are designed in VB and C# are different. Specifically, C# queries are modeled on the idea that they are but a “syntactic transformation”, which means the language spec defines them in terms of translating the query syntax into a different syntax, and all the semantic analysis happens AFTER the final translations are done. In a way this means the language is “hands off” about what things might mean in the middle and doesn’t presume to make guarantees about anything.

In VB on the other hand the language meticulously describes query operators with somewhat strict semantics and may require that objects adhere to certain constraints at intermediate steps which C# might only enforce after the final transformation or not at all if the translation doesn’t require them.

Example questions this forced us to ask during Roslyn include “Do range variables exist?” and “Do range variables have types?”. The answer isn’t clearly the same if you look at one language or the other. e.g. in VB you can explicitly type the variables declared by a Let query operator; in C# you can’t. But, let me give you example of a program whose VB equivalent will not compile in any version of VB but does compile in C# 2012 despite being CRAZY:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CSharpExamples
struct Point
public int X { get { return 0; } }
public int Y { get { return 0; } }
class Foo
public IEnumerable<Point> Select<T>(Func<string, T> selector)
return new Point[0];
static class Program
static void Main(string[] args)
var c = new Foo();
var q = from X in c let Y = "" select Y.CompareTo(1);

Why is this insane, you ask? Well, when X is introduced its type is string. Then the let clause introduces a new range variable Y which is also typed as string. But the underlying query operator doesn’t produce a sequence of anonymous types, it actually produces a sequence of type Point which just happens to have properties X and Y with the same names as our range variables but both of type int and utterly unrelated to the X and Y “declared” in the query so when you refer to Y in the select clause it has type int and the members of int and just… compiles.

That’s what I mean when I say “Do range variables exist and do they have types?”. Prior to VS2015 in C# an argument could be made that the answer is “no”. That said, in Roslyn we actually tightened up the rules in C# here a little bit and this program will no longer compile. Thinking up these two examples has hurt my head enough, whatever other monstrous examples may exist (and I’m sure they do) will have to spring forth from the mind of another.

Why? It’s a tradeoff between the ease and elegance of describing a feature in the specification and to users and implementing it as a simple syntactic transformation and the particularity of the experience you want to create and all the work that entails. I can’t say there’s a right or wrong approach for every situation and the VB and C# design teams and the languages themselves just have different precedents and principles here.

69 & 70. As clause in query From doesn’t not always call cast; bonus from ‘As’ clause can perform implicit user-defined conversions

(Because after the last one this is the shocker, but…)

When you write From x As Integer In y in VB it’s not exactly the same as from int x in y in C#.

First, in C# putting a type there always means you’re casting (or converting) the source collection. There will be a call to .Cast<T>(). In VB it may just be a stylistic choice to eschew type inference and so if the specified type is the element type of the collection no casting is performed.

Secondly, you aren’t limited to conversions which can be statically performed by the .Cast method (known as reference conversions). You can put any type there that the source type can be converted to – including via user-defined conversions and the query will translate to a call to .Cast or .Select appropriately.

Why? No idea. But the VB behavior is very self-consistent. For example, when you type For Each x As T In collection, that As T part may cause any legal conversion to take place or none at all. So the behavior of From range variables and As clauses is consistent with For Each loops (and really, all As clauses).

71-75. Select clause isn’t required at all, can appear in the middle of the query, can appear multiple times, and can declare multiple range variables with implicit names, or explicit names


  • From x In y Where x > 10 is fine. Let’s just say the Select is implicit.
  • From x In y Select x Where x > 10 is perfectly legal.
  • From x In y Select x is really the same as From x In y Select x = x where the x on the left is a new range variable named x and the x on the right is the range variable in scope before the Select. After the Select the old x is out of scope.
  • From x In y Select z = x.ToString(), now x is gone entirely.
  • From x In y Select x.FirstName is really the same as saying From x In y Select FirstName = x.FirstName
  • From x In y Select x.FirstName, x.LastName is sorta like saying From x In y Select New With {x.FirstName, y.LastName} except now no range variables are in scope. But from the perspective of the result of the entire query expression they produce the same IEnumerable(Of $AnonymousType$) so it is almost never needed to explicitly create an anonymous type.

Why? Ask Amanda Silver. But, I can guess!

  • I’m guessing the Select can go anywhere because it’s already jarring from a SQL perspective that the Select clause doesn’t go first, so this way you can put it second. The original design proposal for LINQ in VB did attempt to let you put the Select clause first just like in SQL but from a tooling perspective putting From first was THE best way to go.
  • I’m guessing you can omit it because there’s no reason to require it.
  • I’m guessing you can Select multiple expressions because you can do that in SQL, and it doesn’t make sense to do it any other way, and avoids needing to use anonymous type syntax explicitly. Also there’s a huge precedent in VB for allowing comma separated lists of things.
  • I’m guessing Select implicitly declares names because if it didn’t you’d have to repeat the names and you need names because if there were no names you’d have nothing to refer to in subsequent clauses and projecting subsets of columns from tables in databases is an extremely common scenario.

Does it matter? These differences are top of mind for me because of an error people occasionally get and can’t understand in situations like this:

Module Program
Sub Main()
Dim numbers = {1, 2, 3}
' BC36606: Range variable name cannot match the name of a member of the 'Object' class.
Dim q = From n In numbers
Select n.ToString()
End Sub
End Module

BC36606: Range variable name cannot match the name of a member of the 'Object' class. and BC30978: Range variable '…' hides a variable in an enclosing block or a range variable previously defined in the query expression both can result from unintentionally introducing a range variable with the same name as a member of `Object` or a local variable in scope outside of the query, always in the scenario where the query is selecting a single value which is intended to be anonymous. The workaround is to parenthesize the expression (n.ToString()) because this suppresses the implicit naming. I’d like one day to just have the language not report this error in this common case.

76+. Method invocation and overload resolution are different

I tried… to fit it all… into a one-pager. I wasn’t… strong… enough. Will… post remaining 20-25 differences next week before summer.

39 thoughts on “An Exhausting List of Differences Between VB.NET & C#

  1. Pingback: Exhausting vs exhaustive | Fabulous adventures in coding

  2. Pingback: An Exhausting List of Differences Between VB.NET & C# - How to Code .NET

  3. > If the user desperately wants to not access garbage data they should write it out explicitly so that it’s explicit that they’re asking for a paying for those CPU cycles and so that, in the event that they’ve written a perfectly optimized algorithm that initializes that variable with some non-zero value anyway they’re not paying for both the zero-init and the explicit init.

    C doesn’t specify that the memory should be zeroed-out, but at least on modern *nix systems it is (both malloc and calloc return zeroed-out pages). Is it not the case on Windows?


    • That’s just the thing, “C doesn’t *specify* that it should”. Meaning an implementation may or may not do so. The Microsoft implementation of the CLI /C# compiler/.NET also zeroes the memory but it’s not a requirement that it does and C# designs with the idea that it could be running on a different implementation of the CLR entirely with different rules (e.g. the mono implementation). But the VB language specification explicitly requires it. If an implementation of VB weren’t zero-initing the locals it wouldn’t be a compliant implementation of the language.

      Also, C came out in 1972, a lot has changed since then and the speed of computers has grown so drastically that the performance hit of zeroing out the pages far outweighs the security and stability issues you get by allowing these problems so it makes sense that modern operating systems wouldn’t be so stingy. That said, I don’t work on the Windows team (or at Microsoft at all), and someone from the Windows team or the VC++ would be best to answer you question definitively.

      Liked by 1 person

  4. I’ve only gotten to the first part, but there was a request for comment ^^
    I’m not Japanese, but I am a programmer in Japan that’s using VB, and my co-workers are all Japanese or Chinese. We don’t use full-width characters outside of strings and comments, and I imagine most of us (including me 10 minutes ago) don’t know it was possible in VB.Net (outside of variable names).
    I imagine with the many editors that do syntax highlighting and the many fonts available that are designed with East-Aisan programmers in mind, this feature is more useful for legacy code than new code…


    • Thanks for taking the time to share your experiences. I often wonder about the use of non-Latin characters in general outside of strings. So you do localize your identifiers, though?


      • Nope. I’ve suggested it in the past, but I guess for “we’ve always done it this way” reasons, everything that’s not a comment is generally using ascii compatible characters. We rarely even have strings directly in code using Japanese characters.


  5. This was a good read. Many of these were completely new to me, but even for some that I was aware of, the ‘Why’ of the differences was interesting. It would also be interesting to have the ‘why’ sections include counterpoints from someone more familiar with the C# decisions.


    • Fair enough. I was on the C# design team for a few years and there’s a huge overlap between the two design teams but I’m certainly no expert. If you have particular items you want to dive into you can shoot me a tweet with the item # and we can probably rope the right people into the convo.


  6. Thanks for the post, very interesting read!

    I noticed two small mistakes: “So 1.5 rounds to 2 and 3.5 rounds to 3.” (explaining bankers’ rounding) and “CInt(“3e6″) = 1_000_000”.


    • Yes, it does. But it doesn’t allow the use of extension methods. They may change this in the future though.


  7. This really is exhausting! But interesting. Thank you! It’s been a few years since I’ve used VB.NET, but the one difference I remember catching me until I actually read some documentation was explicit key specification in anonymous types.


  8. Very nice and useful. Thank you Anthony.
    About 46, I tried this:
    Dim i As Object = 0
    Console.WriteLine(i Is Nothing) ‘ False
    Console.WriteLine(i = Nothing) ‘ True

    It is really confusing!


    • i Is Nothing does not compile anymore (tested today)
      And o = Nothing (o is an object) is now exactly the same as o Is Nothing

      So they did change some behavior.


  9. Very nice and useful. Thank you Anthony.
    About 46, I tried this:
    Dim i As Object = 0
    Console.WriteLine(i Is Nothing) ‘ False
    Console.WriteLine(i = Nothing) ‘ True

    It is really confusing!


  10. Found a bug in 59:
    1 + 1 = 2.0
    1 + 1 = 2
    I’ve tested it on .net framework 4.6.1 with the following code:
    Module Program
    Sub Main(args As String())
    MsgBox(TypeName(1 + 1)) ‘ Output: Integer
    End Sub
    End Module


    • Good catch! I found another bug while fixing it. In general whenever converting something to a number for arithmetic, VB uses the `Double` type so the result is of type `Double`, but that’s obviously not the case when just adding two `Integers`. Also, bit shifting doesn’t convert to `Double` but `Long`. FYI, the reason it uses these types is because they’re the biggest floating-point and integral types respectively and therefore there’s the least chance that the conversion will fail due to overflow. Thanks for the heads-up!


  11. I can not wait for the next part! – The announced week seems much longer 🙂


    • Yeah, I tried so hard to put them out back to back but I just had to do some coding. I talked about 2 prototypes already and there are two more I’m working on before I think I’ll be in the mood to dive back into the last 20 or so differences. I probably could have just dripped them out one at a time but I tend to hyperfocus. I should update whatever post I wrote where I said a week. I’m certain I’ll write it in either April or May though. There’s a backlog in my head, I promise!


  12. Pingback: Top-Level Code Prototype: Scenario A | Anthony's blog

  13. Pingback: Pattern-based XML Literals Prototype: Client-Side VB.NET Running in the Browser (sorta) | Anthony's blog

  14. 1) Why does VB not allow overriding events, where C# does?
    2) In VB compiler-generated expression trees, when a method node expects a certain type, but the passed-in expression node is an inheriting or implementing type, the VB compiler wraps the argument expression node with a Convert node to the expected type. (For an example, see .) Why is that?


    • Generally, the express tree conversions model how the language actually represents an operation. In the case of passing an argument to a method where a widening conversion is used from the argument to the parameter type that information is included. It would be just as fair to ask why the C# trees don’t include this information.


      • “Generally, the express tree conversions model how the language actually represents an operation.” So my question is, why does VB “understand” the conversion of a derived type to a subtype as requiring a widening conversion, when this is not reflected in the IL.


      • The language doesn’t reflect IL and the expression trees DEFINITELY don’t reflect IL. They all have different requirements. For instance, in IL the language emits extra conversions around generics with value-type constraints which aren’t “there” to the language because the runtime verifier doesn’t check generic type constraints (I think the verifier doesn’t do this either for performance or because there wasn’t time to implement and test it).

        If you call a method M(Object) and you pass in a string to the language you are performing a conversion from String to object. There’s no arguing that you aren’t. And when performing overload resolution between M(Object) and M(String) one of them requires a Widening conversion and the other requires an Identity conversion and an Identity conversion is better. That process doesn’t make sense if you say that no conversion is happening to the former or that neither of them perform any conversion.

        Now, to the JIT, none of that matters but the JIT never has to perform overload resolution, lots of things don’t matter to it. And other things do matter to IL. So, we know that certain coding patterns require what is a single Try/Catch/Finally block in the language becoming nested Try/Catch/Finally blocks in IL, which should the expression trees reflect if they were expanded to support full statements?

        I think it’s fair to say that these conversions may be surprising to expression tree visitors, and I think there’s a larger issue around VB expression trees not being tested very well and surprising people, but that has nothing to do with IL and everything to do with looking as close as possible to analogous C# expression trees for the sake of simplifying visitors.


      • If I understand correctly, you’re justifying the compiler-generated conversion because as long as the value doesn’t have the same type as the argument, it involves some kind of conversion. But AFAICT this aspect isn’t emphasized by the language — I’m not forced by the language to explicitly cast my String into an IEnumerable(Of Char)-accepting method parameter. Is there any place where VB.NET emphasizes this conversion where C# would not?
        This is particularly odd in light of what you said — “everything to do with looking as close as possible to analogous C# expression trees”. I’m just wondering if there was a deliberate decision to depart from how both languages describe this idiom, and from how it appears in IL.


      • You’re not forced by the language to perform almost any explicit conversions. That has nothing to do with whether they’re happening or not. “Conversion” in the language does *NOT* mean “cast syntax”. So, for example, when you make a For Each loop in both languages, over a non-generic (object-based) collection and you specify a non-object type on the iteration variable both languages consider a conversion to be happening even though neither language requires a cast. “conversion” in this context does not mean CType/DirectCast appearing in code and it’s only lightly related to what IL is emitted.

        Here’s the line of code I think speaks to this mystery:

        This line indicates that C# represents such conversions internally the same way VB does (and it would have to for the Roslyn API to correctly power the IDE) but for `ImplicitReference` conversions strips them out. That’s where the difference happens. There’s no comment as to why. I can’t say whether they always stripped them out or whether they made a change, if it was whether it was a late change or whether the then VB team was consulted or informed but it’s definitely the case that C# *explicitly* goes out of its way to *remove* that information from the expression trees. Perhaps they wanted to more closely model the syntax rather than the semantics? It would kind of make sense given their general philosophy on LINQ and the elegance of syntactic transformation but I can’t be sure. I could ask around.

        The analogous code in VB seems to be here:

        Comparing them it seems VB will strip out redundant identity conversions even if they are explicit whereas C# will preserve them if they are explicit even if they have no effect (and wouldn’t appear in IL). An interesting consequence.

        But my point is, there isn’t a bug in the VB behavior it’s just inconvenient. I’m totally open to proposals to find a way to change it to be more convenient (in fact, I believe we should in many cases) but my original response “the trees model the (semantics of the) language” was accurate. Based on that line of code the C# tree is removing information the language otherwise uses, for an undocumented reason.


    • Oh, in answer to your first question, it was never deemed important enough. It has come up from time to time. But as one C#/VB language designer, Neal, often remarks – there never needs to be a reason why a feature wasn’t done. It just wasn’t. Nobody designed it or implemented or tested it or figured out how it would work with the rest of the language. There are an infinite number of features that aren’t in any language and the majority of them don’t have reasons for not being there–they also don’t have great reasons to be there.

      It has come up from time to time and I don’t think anyone is hard set against it in principle. Please suggest on GitHub.


  15. Thank you for this! I was a classic VB developer who moved to C# at one job, and then went to a company that used VB.Net for years and is slowly moving to C#. I’ve run into to plenty of confusion in the process and this helps a lot.


  16. I found one case where Visual Basic is NOT using bankings rounding.
    This is in the Microsoft.VisualBasic.Format; here x.5 is always rounded UP.

    So Format(0.5, “0”) = 1 (instead of 0).


  17. From my understand, VB.NET and C# handle numeric operations differently. If two bytes are added together in C#, the two numbers first be up converted to integers. No overflow is possible unless you attempt to cast it back to a byte. On the other hand, VB.NET will perform a byte addition and can result in an overflow. This difference is not why one language performs overflow checking and the other does not but it does reduce the overflow occurrences in C#.


  18. Not that I expect this to be done in the current climate, but how difficult would it be to have the For Each prefer a GetEnumerator that returned a IEnumerator(jOf T) over an internally implemented IEnumerator?


    • Is it the non-generic collections from .NET 1.x? I’ve often wondered about it myself. What’s your scenario?


Comments are closed.