by Zoran Horvat
It is common requirement to expose properties of enumeration type. However, these properties are susceptible to a very simple bug. Let’s start with one example:
enum Weather
{
Unknown = 0,
Sunny,
Rainy,
Cloudy,
PartiallyCloudy,
Windy
}
class Weatherman
{
public Weather CurrentWeather { get; set; }
public void Report()
{
Console.WriteLine("Current weather is {0}.", CurrentWeather);
}
}
In this simple object model, a weather forecaster receives information about current state of affairs outside and then simply reports it to the console. The forecaster is represented by the Weatherman class and current weather is represented by a single enumeration value. By default, forecaster’s knowledge about the weather is represented by enumeration value Unknown, with quite an obvious meaning.
Anyway, when Weatherman class is put in motion, that looks as follows:
Weatherman wm = new Weatherman()
{
CurrentWeather = Weather.PartiallyCloudy
};
wm.Report();
The result is:
Current weather is PartiallyCloudy.
Everything seems to be working quite well. However, enumerations hide a surprise. Let’s try this:
Weatherman wm.CurrentWeather = (Weather)17;
wm.Report();
Now, this call produces an unwanted result on the output:
Current weather is 17.
So what has happened here? Enumeration property has received an arbitrary integer value without throwing exception or reporting the problem in any other way. Reason for this seemingly strange behavior is that the underlying type for enumeration is int. Therefore, assigning a value 17 to an enumeration which does not define that value is not an issue: int as any other, as far as the compiler and runtime are concerned.
This behavior is unwanted on two levels. First, we don’t want the enumeration property to receive a value that is not explicitly defined – we want a proper level of control. Second, users would easily notice the problem because further application execution becomes erratic. In more complex cases, invalid enumeration value may cause the application to crash, to get stuck in a state from which it cannot recover, etc.
The simplest way to guarantee that value being assigned to an enumeration variable is valid is to use Enum type’s static method IsDefined. Here is the modified Weatherman class which employs this idea:
class Weatherman
{
public Weather CurrentWeather
{
get
{
return _currentWeather;
}
set
{
if (!Enum.IsDefined(typeof(Weather), value))
throw new System.ArgumentException(
string.Format("Invalid weather state {0}.", value));
_currentWeather = value;
}
}
public void Report()
{
Console.WriteLine("Current weather is {0}.", CurrentWeather);
}
private Weather _currentWeather;
}
...
try
{
Weatherman wm = new Weatherman()
{
CurrentWeather = Weather.PartiallyCloudy
};
wm.Report();
wm.CurrentWeather = (Weather)17;
wm.Report();
}
catch (System.Exception ex)
{
Console.WriteLine("Error: {0}", ex.Message);
}
This code produces more comfortable output:
Current weather is PartiallyCloudy.
Error: Invalid weather state 17.
If we try the solution above on an enumeration with FlagsAttribute applied, it’s not going to work. The problem will be demonstrated on an enhanced weather forecaster who can recognize distinct features of the weather:
[Flags]
enum WeatherConditions
{
Unknown = 0,
Day = 1,
Night = 2,
ClearSky = 4,
Windy = 8,
Cloudy = 16,
PartiallyCloudy = 32,
Rainy = 64,
Sunny = Day | ClearSky,
Starry = Night | ClearSky,
ImminentRain = Cloudy | Windy,
CloudsMask = Cloudy | PartiallyCloudy | Rainy
}
This enumeration defines components of weather, which can then be combined in many ways. Some of the predefined combinations are conveniently provided by the enumeration itself: sunny day, starry night, imminent rain conditions and presence of clouds are defined as enumeration members.
And here is the weatherman implementation, this time with much more features than its previous incarnation:
class WeathermanPro
{
public WeatherConditions Conditions { get; set; }
public bool IsCloudy
{
get
{
return ((Conditions & WeatherConditions.CloudsMask) != 0);
}
}
public bool IsRainImminent
{
get
{
return IsFlagSet(WeatherConditions.ImminentRain);
}
}
public bool IsRaining
{
get
{
return IsFlagSet(WeatherConditions.Rainy);
}
}
public bool IsDay
{
get
{
return IsFlagSet(WeatherConditions.Day);
}
}
public bool IsWindy
{
get
{
return IsFlagSet(WeatherConditions.Windy);
}
}
private bool IsFlagSet(WeatherConditions flag)
{
return ((Conditions & flag) == flag);
}
public bool IsConvenientForOutdoorActivities
{
get
{
return IsDay && !IsRaining && !IsRainImminent && !IsWindy;
}
}
public void Report()
{
StringBuilder report = new StringBuilder();
report.AppendFormat("Current weather is {0}.", Conditions);
if (IsRainImminent)
report.Append(" Bad weather on sight.");
else if (IsConvenientForOutdoorActivities)
report.Append(" Get out and play.");
else
report.Append(" Stay inside.");
Console.WriteLine(report);
}
}
We can try this class on a couple of examples:
WeathermanPro wm = new WeathermanPro()
{
Conditions = WeatherConditions.Day |
WeatherConditions.PartiallyCloudy |
WeatherConditions.Windy
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Rainy |
WeatherConditions.Day
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Night |
WeatherConditions.Windy |
WeatherConditions.Cloudy
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Day |
WeatherConditions.PartiallyCloudy
};
wm.Report();
This demonstration produces the following output:
Current weather is Day, Windy, PartiallyCloudy. Stay inside.
Current weather is Day, Rainy. Stay inside.
Current weather is Night, ImminentRain. Bad weather on sight.
Current weather is Day, PartiallyCloudy. Get out and play.
Observe how smartly each enumeration value was converted to string when it was printed. Anyway, FlagsAttribute makes enumeration validation more difficult. Simple test with Enum.IsDefined does not suffice. We can demonstrate it very easily:
void ReportEnumValue(WeatherConditions value)
{
Console.WriteLine("{0}: {1}defined",
value,
Enum.IsDefined(typeof(WeatherConditions), value) ? "" : "not ");
}
...
ReportEnumValue(WeatherConditions.Day);
ReportEnumValue(WeatherConditions.Cloudy |
WeatherConditions.Windy);
ReportEnumValue(WeatherConditions.Day |
WeatherConditions.Cloudy |
WeatherConditions.Rainy);
This code produces output that might not be foreseen by everyone:
Day: defined
ImminentRain: defined
Day, Cloudy, Rainy: not defined
Plain enumeration value Day is easily recognized by the IsDefined method. Similarly, combined values Cloudy and Windy have been recognized as a combination (ImminentRain), and hence IsDefined returns True for that input. But another combination, which depicts a day like any other in November, fails. IsDefined method is incapable to combine enumeration flags and compare them against a given value. In order to test whether a given combined enumeration value is valid, given the any enumeration type definition, we have to combine enumeration values manually.
Below is the class which can answer the question whether enumeration value is valid or not, for any given enumeration type. It is applicable to any enumeration type which relies on integer values.
class EnumerationValidator
{
public bool IsDefined(Type enumType, int value)
{
bool defined = Enum.IsDefined(enumType, value);
if (!defined && IsEnumTypeFlags(enumType))
defined = IsDefinedCombined(enumType, value);
return defined;
}
private bool IsEnumTypeFlags(Type enumType)
{
object[] attributes =
enumType.GetCustomAttributes(typeof(FlagsAttribute), true);
return attributes != null && attributes.Length > 0;
}
private bool IsDefinedCombined(Type enumType, int value)
{
int mask = 0;
foreach (object enumValue in Enum.GetValues(enumType))
mask = mask | (int)enumValue;
return (mask & value) == value;
}
}
With this helper class at our disposal, we can complete the enhanced weatherman implementation:
class WeathermanPro
{
public WeatherConditions Conditions
{
get
{
return _conditions;
}
set
{
if (!_enumValidator.IsDefined(typeof(WeatherConditions), (int)value))
throw new System.ArgumentException(
string.Format("Invalid weather conditions {0}.", value));
_conditions = value;
}
}
...
private EnumerationValidator _enumValidator
= new EnumerationValidator();
private WeatherConditions _conditions;
}
And here is the demonstration of the new feature:
try
{
WeathermanPro wm = new WeathermanPro()
{
Conditions = WeatherConditions.Day |
WeatherConditions.PartiallyCloudy |
WeatherConditions.Windy
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Rainy |
WeatherConditions.Day
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Night |
WeatherConditions.Windy |
WeatherConditions.Cloudy
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = WeatherConditions.Day |
WeatherConditions.PartiallyCloudy
};
wm.Report();
wm = new WeathermanPro()
{
Conditions = (WeatherConditions)297
};
wm.Report();
}
catch (System.Exception ex)
{
Console.WriteLine("Error: {0}", ex.Message);
}
This code produces output:
Current weather is Day, Windy, PartiallyCloudy. Stay inside.
Current weather is Day, Rainy. Stay inside.
Current weather is Night, ImminentRain. Bad weather on sight.
Current weather is Day, PartiallyCloudy. Get out and play.
Error: Invalid weather conditions 297.
When dealing with properties returning enumeration values, be aware of the fact that underlying type is plain integer. This requires property setters to validate input values against those values defined by the enumeration type. The situation complicates somewhat when FlagsAttribute is applied to the enumeration. In that case, input value is tested against all values in the enumeration, to make sure that there are no bits in it that are not covered by any of the enumeration values.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.