Introduction

On September 23, 1999, NASA lost contact with a spacecraft sent to Mars, the $125 million Mars Climate Orbiter. The spaceship covered millions of kilometers to great precision in order to reach the planet, and due to an error of a few kilometers’ altitude above the surface, entered Mars’ atmosphere and was destroyed. The failure was traced to code doing calculations in imperial units instead of metric units. Most of our crashes may not be so spectacular (or literal), but we do deal with similar defects, and more often than you probably think.

Consider the following exchange: A colleague stops by your cubicle and asks “How often does the server check for new messages?” You reply “Four”. Your colleague is going to look at you funny, isn’t he? Four? Four what? Four minutes? Four milliseconds? As silly as that sounds, I’m willing to bet you have written code that does this. I know I have. If you need to change that frequency from the default, you create a configuration file entry and set its value:

<appSettings>
    <add key=”NewMessagesTimerFrequency” value=”4” />
</appSettings>

What’s the difference? Configuration files are forms of communication, not only to programs, but also to other programmers. And with that snippet, we just told somebody “Four”. If the original coder meant milliseconds, you could be creating a big problem. This article will propose a few best practices to minimize this class of defects.

Practices

  • Practice: Use System.TimeSpan instead of integers.
  • Rationale: Many .NET BCL types’ members accept an integral number of milliseconds, Thread.Sleep for example. These members should be considered deprecated, relics inherited from the Win32 API. This code:
  • Thread.Sleep(TimeSpan.FromMilliseconds(12))

    is more clear than this:

    Thread.Sleep(12)
  • Practice: Use immutable classes or structures to encapsulate value and unit. These types should implement operators as appropriate to support unit-safe arithmetic and comparison.
  • Rationale: Consider this structure:
  • [ImmutableObject(true)]
    public struct Weight : IEquatable<Weight>, IComparable<Weight>
    {
        private static double GramsPerPound = 453.59237038037829803270366517422;
    
        private long _ValueInMG;
    
        public static Weight FromMilligrams(int value)
        {
            return new Weight() { _ValueInMG = value };
        }
    
        public static Weight FromMilligrams(long value)
        {
            return new Weight() { _ValueInMG = value };
        }
    
        public static Weight FromGrams(double value)
        {
            long mg = (long)Math.Round(value * 1000.0, 0);
            return new Weight { _ValueInMG = mg };
        }
    
        public static Weight FromPounds(double value)
        {
            double grams = value * GramsPerPound;
            long mg = (long)Math.Round(grams * 1000.0, 0);
            return new Weight() { _ValueInMG = mg };
        }
    
        public int CompareTo(Weight other)
        {
            return _ValueInMG.CompareTo(other._ValueInMG);
        }
    
        public override bool Equals(object other)
        {
            return other is Weight ? Equals((Weight)other) : false;
        }
    
        public bool Equals(Weight other)
        {
            return _ValueInMG == other._ValueInMG;
        }
    
        public override int GetHashCode()
        {
            return _ValueInMG.GetHashCode();
        }
    
        public double TotalMilligrams
        {
            get { return _ValueInMG; }
        }
    
        public double TotalGrams
        {
            get { return _ValueInMG / 1000.0; }
        }
    
        public double TotalPounds
        {
            get { return TotalGrams / GramsPerPound; }
        }
    
        public static bool operator ==(Weight x, Weight y)
        {
            return x.Equals(y);
        }
    
        public static bool operator !=(Weight x, Weight y)
        {
            return !x.Equals(y);
        }
    
        public static Weight operator +(Weight x, Weight y)
        {
            long newValue = x._ValueInMG + y._ValueInMG;
            return new Weight() { _ValueInMG = newValue };
        }
    
        public static Weight operator -(Weight x, Weight y)
        {
            long newValue = x._ValueInMG - y._ValueInMG;
            return new Weight() { _ValueInMG = newValue };
        }
    
        public static Weight operator *(Weight x, int y)
        {
            long newValue = x._ValueInMG * y;
            return new Weight() { _ValueInMG = newValue };
        }
    
        public static Weight operator *(Weight x, double y)
        {
            double newValue = (double)x._ValueInMG * y;
            long newValueAsLong = (long)Math.Round(newValue, 0);
            return new Weight() { _ValueInMG = newValueAsLong };
        }
    
        public static Weight operator /(Weight x, int y)
        {
            double newValue = (double)x._ValueInMG / (double)y;
            long newValueAsLong = (long)Math.Round(newValue, 0);
            return new Weight() { _ValueInMG = newValueAsLong };
        }
    
        public static Weight operator /(Weight x, double y)
        {
            double newValue = (double)x._ValueInMG / y;
            long newValueAsLong = (long)Math.Round(newValue, 0);
            return new Weight() { _ValueInMG = newValueAsLong };
        }
    
    }

    This type certainly doesn’t guarantee unit-safety, but you just can’t use it without units at least passing through your consciousness. If two weights are expressed as doubles, you can easily subtract pounds from kilograms. Using the Weight structure, subtracting pounds from kilograms requires something like this:

    Weight beforeWeight = Weight.FromPounds(150);
    Weight afterWeight = Weight.FromGrams(160);
    Weight change = afterWeight - beforeWeight;

    Impossible? No way, especially if beforeWeight is set far away from afterWeight. But it does have a way of reminding you what units you are working in, and after you construct the instances, you can use them without the possibility of an apples/oranges mismatch.

    Immutability is important here. If Weight was mutable and featured a method such as ConvertToPounds(), it would have to keep track of what the current unit was; unneeded complexity that could easily lead to the very defects we are trying to prevent. Favor storing the value in a “native unit” and converting as necessary.

    Note also that Weight provides no operator to add or subtract a unit-less value like a double or an int, which would completely defeat its purpose.

  • Practice: Store CLR types in SQL Server if practical.
  • Rationale: There is an obvious problem with the Weight structure above. You can easily store weights in a database as kilograms, and then write this data-access code:
  • Weight w = Weight.FromPounds((double)resultset["weight"]);

    If instead you exploit SQL Server’s ability to store a column as a CLR type, you can store and retrieve instances of the Weight structure itself in your database (and maintain the ability to sort and compare values in that column), thus eliminating another possibly defective unit conversion.

  • Practice: If you must use unit-less types to store unit-bound quantities, make the unit part of the name. Don’t require reference to documentation to find out what the unit is.
  • Rationale: What if your DBA won’t let you store CLR types in your database? Or you have to store a value as a string in a configuration file or a delimited file? Then you have to use integers or floating-point numbers. In this case, use a name that makes the developer think about the unit every time she accesses the value. If the data-access code above looked like this:
  • Weight w = Weight.FromPounds((double)resultset["weight_in_kilograms"]);

    it would be easy to tell that it’s not right. A few extra characters in a name is a small price to pay to prevent a defect.

Conclusion

Software developers have long been aware of the concept of type safety. This publication identifies a related concept of unit safety. While many modern languages are inherently type-safe, I am not aware of any that are unit-safe. It is up to us to use the features of our languages to author types that are less vulnerable to unit conversion defects.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"