Moving from VB to C#: A few surprises

I had been planning my next post to be about the internal tool that we use to collection FxCop backlog stats, but I'm under a bit of a time constraint to document some of our VB to C# migration issues first. There's no reason that those can't be public, so we'll interrupt the planned tool post sequence for something that I'm hoping may interest at least a few folks out there...

FinRad started out as a mainly VB6 shop back in the day. A few years ago, when the decision was made to take the .NET plunge, VB.NET seemed a logical choice given developer familiarity with the language. Fast-forward a few years, though, and C# was starting to look better and better. Some of the reasons for this were technical, but the straw that really broke the camel's back had more to do with human resources. By the late fall of 2006, several potential hires were citing our use of VB as a negative, and it became pretty clear that we would need to switch to C# as our main working language if we wanted to be able to continue to recruit and retain certain types of developers.

A decision to switch all new development to C# was made pretty quickly, but that wasn't really enough to fix the VB "problem". Both our DevLogic and FinLogic teams have very large existing code bases that need to be maintained and, as long as the relevant projects were in VB, most the developers on those teams were going to be spending most of their time reading and writing VB code. In order to address this issue, we really needed to migrate those VB projects to C#, and that wasn't going to be a trivial task.

Given our large VB code base, it was obvious that we would need to find an automated tool to help us with the migration. We evaluated quite a few products and eventually settled on Instant C# from Tangible Software Solutions. Our original selection was based largely on quality of test conversions, but it turned out to have been an inspired choice. We discovered quite a few weird edge cases once we started our real conversions, and the folks1 at Tangible were incredibly responsive to our bug reports and change requests. (For a while there, my scampering to keep up with the near-instantaneous delivery of new Instant C# releases was quite the running gag at DevLogic scrums.)

Unfortunately, despite the enormous help offered by the Instant C# product, there were still quite a few pre- and post-conversion oddities we had to clean up manually. We decided to try to clean up as much as possible before the actual migration for a couple of reasons:

  1. In order to allow us to block commits to our development trunk during the migration without unduly impacting our delivery schedule, the actual migration (from green build to green build) had to be as quick as possible.
  2. We wanted to address the consequences of as many breaking changes as possible before the migration in order to minimize the impact of the actual migration event on code that consumes the libraries being converted.

Luckily, we were starting from a VB baseline that used Option Explicit On, Option Strict On, and Option Compare Binary, as well, as well as treating all warnings as errors. If you're considering migrating to C#, your first step should probably be to get to this baseline before you even consider running your first test conversion. After you've done that, you'll still be facing quite a few challenges. Some of these are documented in the Instant C# FAQ. Most of the others we encountered are listed in the following table, along with the approaches we chose to take in addressing them:

Problem Example Best addressed at migration step...2 Recommended approach
Before During (via tool) After
Accessing a static member using an instance reference Me.SomeStaticMethod() instead of SomeType.SomeStaticMethod() x
(VB 2005+)
  x
(VB 2003)
This is only really a problem if you are using VB 2003 since the more recent VB compilers are capable of issuing a compiler warning for this scenario. If you are using VB 2003 and are planning both a C# migration and a .NET Framework version upgrade, you might want to consider performing the .NET upgrade first so that you can more easily clean up the inappropriate references before your C# migration.
Accessing a member of a VB module without a type reference SomeModuleMethod() instead of SomeModuleType.SomeModuleMethod() x     Replace all VB modules with classes containing static members before performing the C# migration. We used a custom FxCop rule to help us find all the modules.
Declaration of methods with optional parameters Public Sub SomeMethod(Optional ByVal something As Integer = 0)   x   Once you decide to migrate to C#, start using overloads instead of creating new signatures with optional parameters in VB. (FxCop includes a rule, DefaultParametersShouldNotBeUsed, that can help you detect any methods with optional parameters.) It shouldn't be necessary to manually convert existing signatures with optional parameters to overloads if you're using a conversion tool like Instant C#.
Use of methods with optional parameters foo.DoSomething(a, , c)   x  x There are two cases in which this can cause problems. The first is when the method with optional parameters is not defined in your own code base, so the signature with optional parameters is not replaced with overloads at conversion. The good news is that most third party librairies you use were probably written in C#, so they are unlikely to use optional parameters. However, if you reference internal libraries, they are somewhat more likely to have been written in VB. We addressed this issue by starting our C# migration at our "lowest" level of dependency and working "upwards" so that no given VB project is migrated to C# before all its internal dependencies have been migrated.

The second scenario where invocation of the methods with optional parameters can cause problems is when there are gaps in the sequence as in the example to the left. We encountered so few of these that we simply fixed the problems after migration with Instant C#.
Use of named parameters foo.DoSomething(someArgument:=a)   x   We didn't encounter any problems with this so either we weren't using named parameters in method invocations, or Instant C# took care of them with no problems.
Implementation types nested inside interfaces See rule #1 at http://msmvps.com/blogs/calinoiu/archive/2007/06/03/some-fxcop-rules-for-vb.aspx for examples x     All implementation was moved out of interfaces prior to the C# migration. Unfortunately, finding these problems before migration can be a bit tricky (particularly for the case where the nested type is a delegate created by the VB compiler for an event declared on the interface), so we created an FxCop rule to find them for us.
Declaration of properties with arguments Public Property SomeProperty(ByVal foo As String, ByVal bar As Integer) As String x     These were converted to methods prior to the C# migration. We used a custom FxCop rule to help us locate the problem properties (rule #2 at http://msmvps.com/blogs/calinoiu/archive/2007/06/03/some-fxcop-rules-for-vb.aspx).
Properties named "Item" are only recognized as indexers if they are also marked as the type default member Public Property Item(ByVal index As Integer) As Object x     Where applicable, Item properties were marked as default prior to the C# migration. We used a custom FxCop rule to help us locate the problem properties.
Use of MyClass for call scope forcing MyClass.DoSomethingVirtual() instead of Me.DoSomethingNonVirtual() x     These were addressed prior to migration. We used a custom FxCop rule to help us locate the problems (rule #4 at http://msmvps.com/blogs/calinoiu/archive/2007/06/03/some-fxcop-rules-for-vb.aspx).
Member name matches type name A property named Label declared on a class named Label x     These were addressed prior to migration. We used a custom FxCop rule to help us locate the problem members (rule #3 at http://msmvps.com/blogs/calinoiu/archive/2007/06/03/some-fxcop-rules-for-vb.aspx).
Case mismatches Imports SomeNameSpace instead of Imports SomeNamespace x   x We fixed as many as possible of these before migration by running test migrations and fixing the problem in the original VB source code as well as the migrated C# code.
Conditional compilation expressions (as opposed to straight-up use of compilation constants) #IF SOME_VERSION = 1 instead of #IF SOME_VERSION_1 x     These were fixed prior to conversion, preferably by moving the code in question to a method marked with ConditionalAttribute.
Use of C# reserved words for names Dim switch As Boolean x   x These were addressed immediately after conversion by prefixing the variable name with the "@" character if no other non-breaking renaming were possible.
Use of ReDim   x   x Where possible, the code was modified prior to conversion in order to avoid the ReDim. In cases where it couldn't be easily avoided and Instant C# reported conversion problems, Reflector was used to generate a C# version of the original VB code.
Inappropriate use of the Shadows keyword   x   x There are two problems here. The first is that quite a few developers seem to automatically smack on a "Shadows" rather than an "Overloads" when they see compiler warning BC40003 ("<type1> '<membername>' shadows an overloadable member declared in the base <type2> '<classname>. If you want to overload the base method, this method must be declared 'Overloads'."). The second problem is that the VB compiler sometimes requires members to be marked with Shadows even when they're not really shadowing anything.

We fixed at least some of the first category in the VB source code when running test conversions, but we had to wait until post-conversion to fix the latter category.
Use of partial namespace names for qualifying types Dim x As B.C instead of Dim x As A.B.C x   x The VB and C# compilers seem to use different prioritization rules when resolving partial namespaces. To avoid the problem, we started requiring use of complete namespace names (preferably via Imports) as soon as we discovered the issue. We also cleaned up as many instances of the problem as possible in the VB source code during conversion test runs.
Class name starting with an underscore is not CLS-compliant   x     This is detected as a compiler warning by the VB 2005 and VB 2008 compilers, so upgrade before migrating to C# if possible. Otherwise, smack on [CLSCompliant(false] attributes after migration for minimal code churn unless you're dealing with abstract classes or members, in which case fixing the problem before migration would probably be a good idea.
Implicit conversion of enum argument values to integers by VB       x When the VB compiler encounters an invocation of a method with an integer argument in which an enum value is provided instead of an integer value, the compiler will automatically substitute the integer value even if there is an overload that would take a boxed enum value. Sometimes you're lucky, and a compilation error will be seen in C#. In other scenarios, you'll only see a change in runtime behaviour (e.g.: try running Console.WriteLine(AttributeTargets.Class) from VB and C# code).

We briefly considered trying to create an FxCop rule to detect the problem cases, but we weren't sure that we had discovered all the scenarios that would lead to different behaviours between the two compilers, and we ended up fixing the issues by brute force instead. Luckily, we had reasonably good test coverage to help us find problems. Your mileage may vary.
Comparisons to empty strings If someString = String.Empty Then... x     Replace all comparisons to empty strings by explicit verification of the nullity of the string and comparison of the string length to 0 (using String.IsNullOrEmpty where appropriate).3 The main reason for this is that at least the VB 2003 compiler will force use the Microsoft.VisualBasic.CompilerServices.StringType.StrCmp() function to compare strings, and that function treats nulls like empty strings.
Use of CBool or CType(<string>, Boolean) to convert strings to boolean values CBool("1")     x The Microsoft.VisualBasic library methods that implement these VB-specific functions will convert string representations of numbers to boolean values based on whether they represent zero or not. A cast to boolean in C# will not do this. A call to System.Convert.ToBoolean will result in a FormatException in .NET 1.1 but not in .NET 2.0 (at least as of SP1), where the "true if not a representation of zero" logic seems to be applied.
Reflection on inherited non-public members       x The VB and C# compilers sometimes apply slightly different metadata to generated IL. One side effect of this is that an inherited non-public member can be accessed directly as if it were part of a subclass if it comes from an assembly compiled from VB code, but not if it comes from an assembly compiled from C# code. If your application relies on reflection to find a non-public inherited member, the code that is responsible for finding it may need to be modified to look for the member in base types until it finds a match or runs out of base types.
Type and member organization in compiled assembly   x   x If you generate assemblies via the Reflection.Emit approach, and you're not already enforcing verification of the emitted assemblies, you might want to start right away. Of course, there are excellent reasons for this beyond the consequences of a VB to C# migration. However, if your emitted assemblies worked just fine when they referenced your VB-based assemblies, you may find unexpected differences in behaviour after migration of those referenced assemblies to C#. PEVerify can help you find such problems early.
Embedded resource paths       x The VB and C# compilers use different rules for generating the full names of embedded resources, particularly with respect to files that are not at the project root. You may find it necessary to move an embedded resource file into the project root folder if you cannot easily identify and change the places where an embedded resource path is specified in the code.
String manipulation functions   x   x VB string manipulation functions like Left, Right, and StrRev have somewhat different behaviour than their System.String method analogues. For example, Left("abc", 4) returns "abc", but "abc".Substring(0, 4) throws an ArgumentOutOfRangeException. Unless you find this change in behaviour to be desirable, you may want to replace your use of the VB functions by custom methods that conserve the VB behaviour rather than switching to the System.String methods.
Integer vs non-integer division Console.WriteLine(1 / 2) x   x C# defaults to using integer division if both arguments are integral numeric types. VB always uses real number division rules. So far, we've only converted DevLogic assemblies and FinLogic test assemblies, which luckily don't perform a lot of division, so we simply performed a manual review of all division operations. (If you're going to do this, search for division in the VB code, where the slash character isn't a comment marker.)

Unfortunately, our FinLogic projects, being chock full of financial analysis code, contain a heck of a lot more division operations. In order to ensure that we find all non-integer division cases that would be converted to integer division, we'll probably need to create a custom FxCop rule to find the division problems before we can migrate those assemblies.
Equals and GetHashCode   x     In VB, one can override the Equals method without overriding GetHashCode. In C#, this generates a compiler warning. In practice, you may find that most cases where you've overridden Equals without also overriding GetHashCode are generally mistakes, and removing the Equals override may be the best fix. Otherwise, add an appropriate GetHashCode override before the C# migration. The FxCop rule OverrideGetHashCodeOnOverridingEquals can help you find the problems.
Event handling wire-up code     x   C# has no analogues for the VB WithEvents or Handles syntax, which means that a relatively complex conversion with auto-generated class members will be necessary to migrate VB code that uses these keywords. If you view the C# representing the IL generated for such code via Reflector, you'll see what's necessary to successfully migrate the code. Unfortunately, this is one of the weaker areas of Instant C#, and we ended up manually pasting code from Reflector for the few problems that we encountered with the conversions we've done so far. However, we do have one remaining DevLogic project that we've been delaying migrating simply because of the sheer quantity of WithEvents and Handles uses that it contains, so poor Dave at Tangible is probably going to be getting yet another feature request from us in the near-ish future...

Depending on your choice of conversion tools and your target code base, you may encounter more of less problems than we did. Either way, make sure that you lay in a good supply of sacrificial animals before starting your conversion. This is not an activity for which a couple of chickens and a goat are going to suffice!



1I say "folks", but we have a sneaking suspicion that Tangible may very well be one guy named Dave. I've been tempted to ask, but it's probably more fun not knowing. ;) (And with that one sentence, I've probably insulted a great team of wonderful people at Tangible. I'm a terrible, terrible person.)

2If both the "before" and "after" stages are indicated, it is recommended that as much as possible be cleaned up prior to migration, although a double-check after migration would usually be required if code may have been modified between the pre-migration clean-up and the actual migration.

3Actually, you should probably do this even if you're not migrating to C#, but that's a story for another day...

Print | posted on Thursday, February 14, 2008 8:39 AM

Feedback

No comments posted yet.
Title  
Name
Email (never displayed)
Url
Comments   
Please add 4 and 6 and type the answer here: