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#…”

or

“…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 exhaust ive . It’s exhaust ing . 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 exhaust ed . If I or others run into or remember other differences I’ll gladly update this list after the fact.

. It’s exhaust . 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 exhaust . 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.

. 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.

(of which there are countless). I’ll mostly be talking about 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

f-ing magic

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 DomainObject s 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:

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 Module s 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:

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.

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.

Statements

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:

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.

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.

), 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:

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.

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:

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

is shorthand for when you want to test the same value multiple times but… 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:

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:

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.

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.:

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.

Conversions

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.

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.

converts to a 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.

converts to a 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

WHAT!??!?

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.

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.

Expressions

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:

Nothing is taking on a value type, and… 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”:

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:

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:

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?).

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.

to , , , , , or (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?

or Add new suffix syntax ( ? ) which wraps a value in a nullable to help type inference, e.g. If(False, 0?, Nothing)

) which wraps a value in a nullable to help type inference, e.g. 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.

, , , and each meaning different things. 2002 – VB.NET only has Nothing (meaning default value in context) and C# has only null (meaning… null).

(meaning default value in context) and C# has only (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 .

(meaning default value of type ) 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 . 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.

(meaning default value in context) because there are plentiful scenarios where specifying is redundant or impossible; VB continues to resist urge to a true expression (or equivalent) because:

So there you have it.

Off-topic (but related)

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

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:

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:

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 source.roslyn.io 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:

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 .

and are siblings that share a common prefix by convention but does not contain and does not contain . 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.

ComponentModel.PropertyChangedEventArgs 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.:

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.

mean because it is impossible to convert an array into a 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)

to (see Conversions section) Unboxes complex value types directly

Does NOT unbox primitive types directly

unbox primitive types directly Does NOT support dynamic conversions (use CTypeDynamic function)

VB DirectCast

Does NOT support user-defined conversions

support user-defined conversions Supports reference conversions

Does NOT support intrinsic conversions (cannot convert Integer to Byte )

support intrinsic conversions (cannot convert to ) 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:

VB

“1” + 1 = 2.0

“1” & 1 = “11”

C#

"1" + 1 == "11"

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

VB

“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.

C#

"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

VB

1 + 1 = 2

1 & 1 = “11”

C#

1 + 1 == 2

Between String and Enum types

VB

“Today: ” + DayOfWeek.Monday ‘ Exception: String "Today: " cannot be converted to Double.

“Today: ” & DayOfWeek.Monday = “Today: 1”

“Today: ” & DayOfWeek.Monday.ToString() = “Today: Monday”

C#

"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

, and “1” + 1 = 2.0 , but

, 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”.

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?

/ return 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:

Comparison VB C# 1 > NULL NULL FALSE 1 = NULL NULL FALSE 1 >= NULL NULL FALSE NULL > NULL NULL FALSE NULL = NULL NULL TRUE NULL >= NULL NULL FALSE

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.

FALSE AND NULL is FALSE

TRUE OR NULL is TRUE

TRUE AND NULL is NULL

FALSE OR NULL is NULL

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:

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”.

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?

LINQ to SQL

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 a