C# syntax question

Most likely, this is a question that I should know by now, but I’m going to ask anyway.

What’s the difference between the following four methods to assign a variable in a BPM in C#? Is one preferred and why? Or is it purely coding style preference?

string sLaborType;

var ttLaborDtl_row = ttLaborDtl.FirstOrDefault();

if (ttLaborDtl_row != null)
{
  sLaborType = Convert.ToString(ttLaborDtl_row.LaborType);
  sLaborType = Convert.ToString(ttLaborDtl_row["LaborType"]);
  sLaborType = (ttLaborDtl_row.LaborType).ToString();
  sLaborType = (ttLaborDtl_row["LaborType"]).ToString();
}

Bonus question, in this simple example, is there anything else I should be doing differently?

3 Likes

There are actually a couple more styles to consider as well:

sLaborType = ttLaborDtl_row.LaborType;
sLaborType = (string)ttLaborDtl_row["LaborType"];

The general guideline I was taught was to write functional, readable, efficient code, focusing on each aspect in that order.

Is it functional?
All the methods above yield the desired result.

Can someone else understand it?
To varying degrees - I was able to look at your code and recognize what each line was attempting to accomplish. Some were more readable to me that others.

Does it work well?
I made a quick BPM with similar structure and ran a stopwatch for each variable assignment. Here are the results:

  • Convert.ToString(row.col) 0ms
  • Convert.ToString(row["col"]) 0.0123ms
  • (row.col).ToString() 0ms
  • (row["col"]).ToString() 0.0017ms
  • row.col 0ms
  • (string)row["col"] 0.0017ms

As you can see, the best performing methods were when calling using a row.col format. This is because the Epicor engineers went through the trouble of defining the temp table objects. The compiler already knows that ttLaborDtl_row.LaborType is a string because the LaborType object within the Erp.Bpm.TempTables.LaborDtlTempRow (the underlying type of your ttLaborDtl_row object) is defined as such and so doesn’t have to work through a cast/convert operation. This is one of the (many) benefits of a strongly-typed programming language.

Conversely, when the data was called via a row["col"] syntax, the type coming back could be anything, so we have to actually read the data to know what’s coming back and then cast/convert it to the desired type. Also, a native cast (.ToString() or (string)) will be faster than a Convert method because it doesn’t have to handle as much. Use the Cast method if possible and then go to Convert if necessary (i.e. going from a string to an integer).

All that said, the “best” way is the what works for you. Personally, I try to know what the object types are (you can always guess and it’ll tell you if you’re wrong when you hit the Check Syntax button) and call them using the simple row.col format.

4 Likes

Just adding more options for debate:

sLaborType = (string)ttLaborDtl_row.LaborType;
sLaborType = (string)ttLaborDtl_row["LaborType"];
1 Like

@jstephens, Thank you! we replied at the same time.

1 Like

Excellent, logical, clear analysis. Thankyou Jason

To add to the above explanations,
another point which is also as important (if not more) than efficiency it is how “safe” your calls are against crashes and exceptions. We all know our users use our programs exactly as designed and NEVER try anything crazy… right?

When you do

(string)row["col"]

You are doing a cast you are telling the compiler that the object stored at row[“col”] is a string and you as the developer are guaranteeing this 100%. This is fine as long as a) that object is in fact a string and b) there is actually an object there. If you do the above on a null object or an integer you will get a runtime exception and your program will crash. That is because casting puts the onus on you to ensure that the data there is both valid and it exists.

Using the Convert.ToString() method avoids this, in the Convert.ToString() method the specification handles nulls gracefully and thus if you try to convert a null it will not crash your application,it just returns an Empty string. Further more it knows how to convert any primitive (int, decimal, long, double, date etc) into a string so it won’t crash like a cast would if the type of the object insn’t already a string. So although it does a bit more work and it is “less efficient” it also protects you from runtime errors and crashes.

Now using the ToString() method is a little different than casting and Convert.ToString() when you say .ToString() you are asking the interpreter to give you the string representation of the existing object. So it carries the same error prone behavior as a cast where by if the object is null it will throw a null object exception. However the difference between the cast and it is that the ToString() method can be overridden in your own classes. Take this class for example

public class Car
{ 
       string Make {get;set;}
       string Model {get;set;}
       public Car(string make, string model)
       { 
              this.Make = make;
              this.Model = model;
       }
 
    public override string ToString()
    {
        return string.Format("This is a Car made by: {0} and the Model of the Car is: {1}", this.Make, this.Model);
    }
}

So now if anyone uses the Car class and does a ToString() on it they will get the “String” representation of what a Car is as demonstrated in this snippet

That is to say that ToString() will not always return the “String” you expect, and again it is error prone if the object is null.

So although I agree that you should do what works for you, you should strive for error free code as much as possible, so when you are able to I would recommend using the Convert.ToString() method unless you are 100% sure that your object will ALWAYS exist and or that it is in fact a string.

You can obviously avoid these pitfalls by checking for null yourself or by using the null conditional operator.

var x = myObject?.ToString()

Which tells the compiler only do the stuff after the ? If the object isn’t null.

As stated above there are a thousand ways to skin a cat. I’m sure this was way more than you wanted to know as it relates to your original snippet.

11 Likes

I love how @josecgomez mentioned going beyond the simple line and thinking about error cases. I’ll do a similar and say to think about memory allocations.

For those who have not played with it, there is an open source testing package named BenchmarkDotNet that I am absolutely in love with.

You need to attribute up the class and method and it does all the warmup, stddev, median, mean, etc.

What sets this apart is that it also looks at GC impact. The Garbage Collector in dotNet is wonderful in that it prevents most memory leaks but can end up sucking a large part of your cpu by just cleaning up sloppy coding practices. I have witnessed 30% of my server CPU spent in GC collection on some on alpha builds due to a simple misuse of string.

Sample project using the above code snippets:
StringTester.zip (33.5 KB)

The results are printed to the console with a nice summary:

Notice the Allocated column. That’s the thing to observe. It you have two ways of doing the same thing and the times are similar, also look at the memory abuse your code is doing to your system. While this single line is probably not that big of a deal, larger algorithms in long running processes with love you dearly - MRP for example.

To add the benchmark, the nice thing is you just need to attribute the class and method:

Code
[MemoryDiagnoser]
public class StringTest
{
    LaborDtlRow row = new LaborDtlRow();

    [Benchmark]
    public string ConvertToStringProperty() => Convert.ToString(row.JobType);

    [Benchmark]
    public string ConvertToStringViaNamedIndex() => Convert.ToString(row["JobType"]);

    [Benchmark]
    public string ToStringProperty() => (row.JobType).ToString();

    [Benchmark]
    public string ToStringNamedIndex() => (row["JobType"]).ToString();

    [Benchmark]
    public string ViaProperty() => row.JobType;

    [Benchmark]
    public string StringCastviaNamedIndex() => (string)row["JobType"];
}
10 Likes

Thanks so much for the information (@jstephens, @josecgomez, @Bart_Elia), it was all very informative.

I’ll also add that

iAsmSeq = Convert.ToInt32(ttLaborDtl_row["AsmSeq"]);

Will pass the Syntax check in a BPM, but not have a value when running. Where as

    iAsmSeq = Convert.ToInt32(ttLaborDtl_row.AsmSeq);

Will fail the syntax check with, “CS1061 ‘LaborDtlRow’ does not contain a definition for ‘AsmSeq’ and no extension method ‘AsmSeq’ accepting a first argument of type ‘LaborDtlRow’ could be found (are you missing a using directive or an assembly reference?)”

Which is much more helpful when troubleshooting.

3 Likes

Thanks for asking that.
It’s something I’ve wondered myself, but it was never burning enough to ask.
That was some great information.

1 Like

I can now go back to bed…I have learnt my 1 thing today…:slight_smile: …Thanks everyone.

I also now can also contemplate all my wrongdoings in past written code :frowning:

1 Like

I think having code that doesn’t generate a neat/tidy error message isn’t such a bad thing, if it’s a case where it indicates bad magumbo is going on in your database.

For example, if you’ve got an OrderRel row and you’re reading the related OrderHed - if the OrderHed doesn’t exist, you’ve got some corruption in your database. If you give the user a nice clean message “The Order Header doesn’t exist, please contact your friendly IT person.”, who knows when you’re going to get the call. If the user gets some obnoxious message that the user can’t make heads or tails from, you’re going to get a frantic call. If I’ve got orphan OrderRel rows out there, I want the frantic call.

I’d say the same with these examples. If I’ve got a NULL field in a string that should never have nulls, I want that phone call.

Just my two cents.

Well… you asked… here is one thing that I would do if the only thing I wanted was the LaborType column… this makes it one line of code… if there is nothing found, then sLaborType will be blank.

string sLaborType = ttLaborDtl.Select(t=>t.LaborType).FirstOrDefault() ?? "";
4 Likes