Chapter 3. Data Types

3.0 Introduction

Simple types are value types that are a subset of the built-in types in C#, although, in fact, the types are defined as part of the .NET Framework Class Library (.NET FCL). Simple types are made up of several numeric types and a bool type. Numeric types consist of a decimal type (decimal), nine integral types (byte, char, int, long, sbyte, short, uint, ulong, and ushort), and two floating-point types (float and double). Table 3-1 lists the simple types and their fully qualified names in the .NET Framework.

Table 3-1. The simple data types
Fully qualified name Alias Value range
System.Boolean bool true or false
System.Byte byte 0 to 255
System.SByte sbyte -128 to 127
System.Char char 0 to 65535
System.Decimal decimal -79,228,162,514,264,337,593,543,950,335 to 79,228,162,514,264,337,593,543,950,335
System.Double double -1.79769313486232e308 to 1.79769313486232e308
System.Single float -3.40282347E+38 to 3.40282347E+38
System.Int16 short -32768 to 32767
System.Uint16 ushort 0 to 65535
System.Int32 int -2,147,483,648 to 2,147,483,647
System.UInt32 uint 0 to 4,294,967,295
System.Int64 long -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
System.UInt64 ulong 0 to 18,446,744,073,709,551,615

When you are dealing with floating-point data types, precision can be more important than the range of the data values. The precision of the floating-point data types is listed in Table 3-2.

Table 3-2. Floating-point precision
Floating-point type Precision
System.Single (float) 7 digits
System.Double (double) 15–16 digits
System.Decimal (decimal) 28–29 digits

When trying to decide between using floats and decimals, consider the following:

  • Floats were designed for scientists to represent inexact quantities over the entire range of precisions and magnitudes used in physics.

  • Decimals were designed for use by ordinary humans who do math in base10 and do not require more than a handful of digits past the decimal point, or who are keeping track of money in situations where every penny counts (such as reconciling a checkbook).

The C#-reserved words for the various data types are simply aliases for the fully qualified type name. Therefore, it does not matter whether you use the type name or the reserved word: the C# compiler will generate identical code.

Note that sbyte, ushort, uint, and ulong are not compliant with the Common Language Specification (CLS), and as a result, they might not be supported by other .NET languages. Enumerations implicitly inherit from System.Enum, which in turn inherits from System.ValueType. Enumerations have a single use: to describe items of a specific group. For example, red, blue, and yellow could be defined by the enumeration ShapeColor; likewise, square, circle, and triangle could be defined by the enumeration Shape. These enumerations would look like the following:

enum ShapeColor
{
    Red, Blue, Yellow
}

enum Shape
{
    Square = 2, Circle = 4, Triangle = 6
}

Each item in the enumeration receives a numeric value regardless of whether you assign one or not. Since the compiler automatically adds the numbers starting with zero and increments by one for each item in the enumeration, the ShapeColor enumeration previously defined would be exactly the same if it were defined in the following manner:

enum ShapeColor
{
    Red = 0,Blue = 1,Yellow = 2
}

Enumerations are good code-documenting tools. For example, it is more intuitive to write the following:

ShapeColor currentColor = ShapeColor.Red;

instead of this:

int currentColor = 0;

Either mechanism will work, but the first method is easy to read and understand, especially for a new developer taking over someone else’s code. It also has the benefit of being type-safe in C#, which the use of raw ints does not provide. The CLR sees enumerations as members of their underlying types, so it is not type-safe for all languages.

3.1 Encoding Binary Data as Base64

Problem

You have a byte[] representing some binary information, such as a bitmap. You need to encode this data into a string so that it can be sent over a binary-unfriendly transport, such as email.

Solution

Using the static method Convert.ToBase64String on the Convert class, you can encode a byte[] to its String equivalent:

static class DataTypeExtMethods
{
    public static string Base64EncodeBytes(this byte[] inputBytes) =>
        (Convert.ToBase64String(inputBytes));
}

Discussion

Converting a string into its base64 representation has several uses. It allows binary data to be embedded in nonbinary files such as XML and email messages. Base64-encoded data can also be transmitted via HTTP, GET, and POST requests in a more compact format than hex encoding. It is important to understand that data that is converted to base64 format is only obfuscated, not encrypted. To securely move data from one place to another, you should use the cryptography algorithms available in the FCL. For an example of using the FCL cryptography classes, see Recipe 11.4.

The Convert class makes encoding between a byte[] and a String a simple matter. The parameters for this method are quite flexible, enabling you to start and stop the conversion at any point in the input byte array.

To encode a bitmap file into a string that can be sent to some destination, you can use the EncodeBitmapToString method:

public static string EncodeBitmapToString(string bitmapFilePath)
{
    byte[] image = null;
    FileStream fstrm =
        new FileStream(bitmapFilePath,
                        FileMode.Open, FileAccess.Read);
    using (BinaryReader reader = new BinaryReader(fstrm))
    {
        image = new byte[reader.BaseStream.Length];
        for (int i = 0; i < reader.BaseStream.Length; i++)
            image[i] = reader.ReadByte();
    }
    return image.Base64EncodeBytes();
}
Warning

The MIME standard requires that each line of the base64-encoded string be 76 characters long. To send the bmpAsString string as an embedded MIME attachment in an email message, you must insert a CRLF on each 76-character boundary.

The code to turn a base64-encoded string into a MIME-ready string is shown in the following MakeBase64EncodedStringForMime method:

public static string MakeBase64EncodedStringForMime(string base64Encoded)
{
    StringBuilder originalStr = new StringBuilder(base64Encoded);
    StringBuilder newStr = new StringBuilder();
    const int mimeBoundary = 76;
    int cntr = 1;
    while ((cntr * mimeBoundary) < (originalStr.Length - 1))
    {
        newStr.AppendLine(originalStr.ToString(((cntr - 1) * mimeBoundary),
            mimeBoundary));
        cntr++;
    }
    if (((cntr - 1) * mimeBoundary) < (originalStr.Length - 1))
    {
        newStr.AppendLine(originalStr.ToString(((cntr - 1) * mimeBoundary),
            ((originalStr.Length) - ((cntr - 1) * mimeBoundary))));
    }
    return newStr.ToString();
}

To decode an encoded string to a byte[], see Recipe 3.2.

See Also

Recipe 3.2, and the “Convert.ToBase64CharArray Method” topic in the MSDN documentation.

3.2 Decoding a Base64-Encoded Binary

Problem

You have a String that contains information such as a bitmap encoded as base64. You need to decode this data (which may have been embedded in an email message) from a String into a byte[] so that you can access the original binary.

Solution

Using the static method Convert.FromBase64String on the Convert class, you can decode an encoded String to its equivalent byte[] as follows:

static class DataTypeExtMethods
{
    public static byte[] Base64DecodeString(this string inputStr)
    {
        byte[] decodedByteArray =
                 Convert.FromBase64String(inputStr);
        return (decodedByteArray);
    }
}

Discussion

The static FromBase64String method on the Convert class makes decoding a base64-encoded string a simple matter. This method returns a byte[] that contains the decoded elements of the String.

If you receive a file via email, such as an image file (.bmp) that has been converted to a string, you can convert it back into its original bitmap file using something like the following:

// Use the encoding method from 3.1 to get the encoded byte array
string bmpAsString = EncodeBitmapToString(@"CSCBCover.bmp");
//Get a temp file name and path to write to
string bmpFile = Path.GetTempFileName() + ".bmp";

// decode the image with the extension method
byte[] imageBytes = bmpAsString.Base64DecodeString();
FileStream fstrm = new FileStream(bmpFile,
                    FileMode.CreateNew, FileAccess.Write);
using (BinaryWriter writer = new BinaryWriter(fstrm))
{
    writer.Write(imageBytes);
}

In this code, the bmpAsString variable was obtained from the code in the Discussion section of Recipe 3.3. The imageBytes byte[] is the bmpAsString String converted back to a byte[], which can then be written back to disk.

To encode a byte[] to a String, see Recipe 3.1.

See Also

Recipe 3.1, and the “Convert.FromBase64CharArray Method” topic in the MSDN documentation.

3.3 Converting a String Returned as a Byte[] Back into a String

Problem

Many methods in the FCL return a byte[] because they are providing a byte stream service, but some applications need to pass strings over these byte stream services. Some of these methods include:

System.Diagnostics.EventLogEntry.Data
System.IO.BinaryReader.Read
System.IO.BinaryReader.ReadBytes
System.IO.FileStream.Read
System.IO.FileStream.BeginRead
System.IO.MemoryStream // Constructor
System.IO.MemoryStream.Read
System.IO.MemoryStream.BeginRead
System.Net.Sockets.Socket.Receive
System.Net.Sockets.Socket.ReceiveFrom
System.Net.Sockets.Socket.BeginReceive
System.Net.Sockets.Socket.BeginReceiveFrom
System.Net.Sockets.NetworkStream.Read
System.Net.Sockets.NetworkStream.BeginRead
System.Security.Cryptography.CryptoStream.Read
System.Security.Cryptography.CryptoStream.BeginRead

In many cases, this byte[] might contain ASCII- or Unicode-encoded characters. You need a way to recombine this byte[] to obtain the original string.

Solution

To convert a byte array of ASCII values to a complete string, use the GetString method on the ASCII Encoding class:

byte[] asciiCharacterArray = {128, 83, 111, 117, 114, 99, 101,
                            32, 83, 116, 114, 105, 110, 103, 128};
string asciiCharacters = Encoding.ASCII.GetString(asciiCharacterArray);

To convert a byte array of Unicode values to a complete string, use the GetString method on the Unicode Encoding class:

byte[] unicodeCharacterArray = {128, 0, 83, 0, 111, 0, 117, 0, 114, 0, 99, 0,
                            101, 0, 32, 0, 83, 0, 116, 0, 114, 0, 105, 0, 110,
                        0, 103, 0, 128, 0};
string unicodeCharacters = Encoding.Unicode.GetString(unicodeCharacterArray);

Discussion

The GetString method of the Encoding class (returned by the ASCII property) converts 7-bit ASCII characters contained in a byte array to a string. Any value larger than 127 (0x7F) will be ANDed with the value 127 (0x7F), and the resulting character value will be displayed in the string. For example, if the byte[] contains the value 200 (0xC8), this value will be converted to 72 (0x48), and the character equivalent of 72 (0x48), H, will be displayed. The Encoding class can be found in the System.Text namespace. The GetString method is overloaded to accept additional arguments as well. The overloaded versions of the method convert all or part of a string to ASCII and then store the result in a specified range inside a byte[].

The GetString method returns a string containing the converted byte[] of ASCII characters.

The GetString method of the Encoding class (returned by the Unicode property) converts Unicode characters into 16-bit Unicode values. The Encoding class can be found in the System.Text namespace. The GetString method returns a string containing the converted byte[] of Unicode characters.

See Also

The “ASCIIEncoding Class” and “UnicodeEncoding Class” topics in the MSDN documentation.

3.4 Passing a String to a Method That Accepts Only a Byte[]

Problem

Many methods in the FCL accept a byte[] consisting of characters instead of a string. Some of these methods include:

System.Diagnostics.EventLog.WriteEntry
System.IO.BinaryWriter.Write
System.IO.FileStream.Write
System.IO.FileStream.BeginWrite
System.IO.MemoryStream.Write
System.IO.MemoryStream.BeginWrite
System.Net.Sockets.Socket.Send
System.Net.Sockets.Socket.SendTo
System.Net.Sockets.Socket.BeginSend
System.Net.Sockets.Socket.BeginSendTo
System.Net.Sockets.NetworkStream.Write
System.Net.Sockets.NetworkStream.BeginWrite
System.Security.Cryptography.CryptoStream.Write
System.Security.Cryptography.CryptoStream.BeginWrite

In many cases, you might have a string that you need to pass into one of these methods or some other method that accepts only a byte[]. You need a way to break up this string into a byte[].

Solution

To convert a string to a byte[] of ASCII values, use the GetBytes method on the ASCII Encoding class:

byte[] asciiCharacterArray = {128, 83, 111, 117, 114, 99, 101,
                            32, 83, 116, 114, 105, 110, 103, 128};
string asciiCharacters = Encoding.ASCII.GetString(asciiCharacterArray);

byte[] asciiBytes = Encoding.ASCII.GetBytes(asciiCharacters);

To convert a string to a byte[] of Unicode values, use the GetBytes method on the Unicode Encoding class:

byte[] unicodeCharacterArray = {128, 0, 83, 0, 111, 0, 117, 0, 114, 0, 99, 0,
                            101, 0, 32, 0, 83, 0, 116, 0, 114, 0, 105, 0, 110,
                            0, 103, 0, 128, 0};
string unicodeCharacters = Encoding.Unicode.GetString(unicodeCharacterArray);

byte[] unicodeBytes = Encoding.Unicode.GetBytes(unicodeCharacters);

Discussion

The GetBytes method of the Encoding class (returned by the ASCII property) converts ASCII characters—contained in either a char[] or a string—into a byte[] of 7-bit ASCII values. Any value larger than 127 (0x7F) is converted to the ? character. The Encoding class can be found in the System.Text namespace. The GetBytes method is overloaded to accept additional arguments as well. The overloaded versions of the method convert all or part of a string to ASCII and then store the result in a specified range inside a byte[], which is returned to the caller.

The GetBytes method of the Encoding class (returned by the Unicode property) converts Unicode characters into 16-bit Unicode values. The Encoding class can be found in the System.Text namespace. The GetBytes method returns a byte[], each element of which contains the Unicode value of a single character of the string.

A single Unicode character in the source string or in the source char[] corresponds to two elements of the byte[]. For example, the following byte[] contains the ASCII value of the letter S:

byte[] sourceArray = {83};

However, for a byte[] to contain a Unicode representation of the letter S, it must contain two elements. For example:

byte[] sourceArray2 = {83, 0};

The Intel architecture uses a little-endian encoding, which means that the first element is the least significant byte, and the second element is the most significant byte. Other architectures may use big-endian encoding, which is the opposite of little-endian encoding. The UnicodeEncoding class supports both big-endian and little-endian encodings. Using the UnicodeEncoding instance constructor, you can construct an instance that uses either big-endian or little-endian ordering. You do so by using one of the two following constructors:

public UnicodeEncoding (bool bigEndian, bool byteOrderMark);
public UnicodeEncoding (bool bigEndian, bool byteOrderMark,
                        bool throwOnInvalidBytes);

The first parameter, bigEndian, accepts a Boolean argument. Set this argument to true to use big-endian or false to use little-endian.

In addition, you have the option to indicate whether a byte order mark preamble should be generated so that readers of the file will know which endianness is in use.

See Also

The “ASCIIEncoding Class” and “UnicodeEncoding Class” topics in the MSDN documentation.

3.5 Determining Whether a String Is a Valid Number

Problem

You have a string that possibly contains a numeric value. You need to know whether this string contains a valid number.

Solution

Use the static TryParse method of any of the numeric types. For example, to determine whether a string contains a double, use the following method:

string str = "12.5";
double result = 0;
if(double.TryParse(str,
        System.Globalization.NumberStyles.Float,
        System.Globalization.NumberFormatInfo.CurrentInfo,
        out result))
{
      // Is a double!
}

Discussion

This recipe shows how to determine whether a string contains only a numeric value. The TryParse method returns true if the string contains a valid number without the exception that you will get if you use the Parse method.

See Also

The “Parse” and “TryParse” topics in the MSDN documentation.

3.6 Rounding a Floating-Point Value

Problem

You need to round a number to a whole number or to a specific number of decimal places.

Solution

To round any number to its nearest whole number, use the overloaded static Math.Round method, which takes only a single argument:

int i = (int)Math.Round(2.5555); // i == 3

If you need to round a floating-point value to a specific number of decimal places, use the overloaded static Math.Round method, which takes two arguments:

double dbl = Math.Round(2.5555, 2); // dbl == 2.56

Discussion

The Round method is easy to use; however, you need to be aware of how the rounding operation works. The Round method follows IEEE Standard 754, section 4. This means that if the number being rounded is halfway between two numbers, the Round operation will always round to the even number. This example illustrates the standard:

double dbl1 = Math.Round(1.5); // dbl1 == 2
double dbl2 = Math.Round(2.5); // dbl2 == 2

Notice that 1.5 is rounded up to the nearest even whole number (2) and 2.5 is rounded down to the nearest even whole number (also 2). Keep this in mind when using the Round method.

Note

This method is known as Banker’s Rounding; it was invented because it introduces less bias when you’re rounding large sets of numbers that include halves, as sets containing currencies often do.

See Also

The “Math Class” topic in the MSDN documentation.

3.7 Choosing a Rounding Algorithm

Problem

The Math.Round method will round the value 1.5 to 2; however, it will also round the value 2.5 to 2. You might prefer to round to the greater number (e.g., round 2.5 to 3 instead of 2). Conversely, you might prefer to round to the lesser number (e.g., round 1.5 to 1).

Solution

Use the static Math.Floor method to always round up when a value is halfway between two whole numbers:

public static double RoundUp(double valueToRound) =>
    Math.Floor(valueToRound + 0.5);

Use the following technique to always round down when a value is halfway between two whole numbers:

public static double RoundDown(double valueToRound)
{
    double floorValue = Math.Floor(valueToRound);
    if ((valueToRound - floorValue) > .5)
        return (floorValue + 1);
    else
        return (floorValue);
}

Discussion

The static Math.Round method rounds to the nearest even number. However, there are times that you do not want to round a number in this manner. The static Math.Floor method can be used to allow for different manners of rounding.

Note

The methods used to round numbers in this recipe do not round to a specific number of decimal points; rather, they round to the nearest whole number.

See Also

The “Math Class” topic in the MSDN documentation.

3.8 Safely Performing a Narrowing Numeric Cast

Problem

You need to cast a value from a larger value to a smaller one, while gracefully handling conditions that result in a loss of information. For example, casting a long to an int results in a loss of information only if the long data type is greater than int.MaxSize.

Solution

The simplest way to handle this scenario is to use the checked keyword. The following extension method accepts two long data types and attempts to add them together. The result is stuffed into an int data type. If an overflow condition exists, the OverflowException is thrown:

public static class DataTypeExtMethods
{
    public static int AddNarrowingChecked(this long lhs, long rhs) =>
        checked((int)(lhs + rhs));
}

// Code that uses the extension method
long lhs = 34000;
long rhs = long.MaxValue;
try
{
    int result = lhs.AddNarrowingChecked(rhs);
}
catch(OverflowException)
{
    // could not be added
}

This is the simplest method. However, if you do not want the overhead of throwing an exception and having to wrap a lot of code in try-catch blocks to handle the overflow condition, you can use the MaxValue and MinValue fields of each type. You can perform a check using these fields prior to the conversion to ensure that no information is lost. If the cast will cause an information loss, the code can inform the application beforehand. You can use the following conditional statement to determine whether sourceValue can be cast to a short without losing any information:

// Our two variables are declared and initialized.
int sourceValue = 34000;
short destinationValue = 0;

// Determine if sourceValue will lose information in a cast to a short.
if (sourceValue <= short.MaxValue && sourceValue >= short.MinValue)
    destinationValue = (short)sourceValue;
else
{
    // Inform the application that a loss of information will occur.
}

Discussion

A narrowing conversion occurs when a larger type is cast down to a smaller type. For instance, consider casting a value of type Int32 to a value of type Int16. If the Int32 value is less than or equal to the Int16.MaxValue field and the Int32 value is greater than or equal to the Int16.MinValue field, the cast will occur without error or loss of information. Loss of information occurs when the Int32 value is greater than the Int16.MaxValue field or the Int32 value is less than the Int16.MinValue field. In either case, the most significant bits of the Int32 value are truncated and discarded, changing the value after the cast.

If a loss of information occurs in an unchecked context, it will occur silently without the application noticing. This problem can cause some very insidious bugs that are hard to track down. To prevent this, check the value to be converted to determine whether it is within the lower and upper bounds of the type that it will be cast to. If the value is outside these bounds, then code can be written to handle this situation. This code could prevent the cast from occurring and/or inform the application of the casting problem. This solution can help prevent hard-to-find arithmetic bugs from creeping into your applications.

Both techniques shown in the Solution section are valid. However, the technique you use will depend on whether you expect to hit the overflow case on a regular basis or only occasionally. If you expect to hit the overflow case quite often, you might want to choose the second technique of manually testing the numeric value. Otherwise, it might be easier to use the checked keyword, as in the first technique.

Note

In C#, code can run in either a checked or unchecked context; by default, the code runs in an unchecked context. In a checked context, any arithmetic and conversions involving integral types are examined to determine whether an overflow condition exists. If so, an OverflowException is thrown. In an unchecked context, no OverflowException will be thrown when an overflow condition exists.

You can set up a checked context by using the /checked{+} compiler switch to set the Check for Arithmetic Overflow/Underflow project property to true, or by using the checked keyword. You can set up an unchecked context by using the /checked- compiler switch to set the Check for Arithmetic Overflow/Underflow project property to false, or by using the unchecked keyword.

You should be aware of the following when performing a conversion:

  • Casting from a float, double, or decimal type to an integral type results in the truncation of the fractional portion of this number. Furthermore, if the integral portion of the number exceeds MaxValue for the target type, the result will be undefined unless the conversion is done in a checked context, in which case it will trigger an OverflowException.

  • Casting from a float or double to a decimal results in the float or double being rounded to 28 decimal places.

  • Casting from a double to a float results in the double being rounded to the nearest float value.

  • Casting from a decimal to a float or double results in the decimal being rounded to the resulting type (float or double).

  • Casting from an int, uint, or long to a float could result in the loss of precision, but never magnitude.

  • Casting from a long to a double could result in the loss of precision, but never magnitude.

See Also

The “Checked Keyword” and “Checked and Unchecked” topics in the MSDN documentation.

3.9 Testing for a Valid Enumeration Value

Problem

When you pass a numeric value to a method that accepts an enumeration type, it is possible to pass a value that does not exist in the enumeration. You want to perform a test before using this numeric value to determine if it is indeed one of the ones defined in this enumeration type.

Solution

To prevent this problem, test for the specific enumeration values that you allow for the enumeration-type parameter using a switch statement to list the values.

Using the following Language enumeration:

public enum Language
{
    Other = 0, CSharp = 1, VBNET = 2, VB6 = 3,
    All = (Other | CSharp | VBNET | VB6)
}

Suppose you have a method that accepts the Language enumeration, such as the following method:

public void HandleEnum(Language language)
{
    // Use language here...
}

You need a method to define the enumeration values you can accept in HandleEnum. The CheckLanguageEnumValue method shown here does that:

public static bool CheckLanguageEnumValue(Language language)
{
    switch (language)
    {
        // all valid types for the enum listed here
        // this means only the ones we specify are valid
        // not any enum value for this enum
        case Language.CSharp:
        case Language.Other:
        case Language.VB6:
        case Language.VBNET:
            break;
        default:
            Debug.Assert(false, 
                $"{language} is not a valid enumeration value to pass.");
            return false;
    }
    return true;
}

Discussion

Although the Enum class contains the static IsDefined method, it should not be used. IsDefined uses reflection internally, which incurs a performance penalty. Also, versioning of the enumeration is not handled well. Consider the scenario in which you add the value ManagedCPlusPlus to the Languages enum in the next version of your software. If IsDefined is used to check the argument here, it will allow MgdCpp as a valid value, since it is defined in the enumeration, even though the code for which you are validating the parameter is not designed to handle it. By being specific with the switch statement shown in CheckLanguageEnumValue, you reject the MgdCpp value, and the code does not try to run in an invalid context. This is, after all, what you were after in the first place.

The enumeration check should always be used whenever the method is visible to external objects. An external object can invoke methods with public visibility, so any enumerated value passed in to this method should be screened before it is actually used.

Methods with private visibility may not need this extra level of protection. Use your own judgment on whether to use the CheckLanguageEnumValue method to evaluate enumeration values passed in to private methods.

The HandleEnum method can be called in several different ways, two of which are shown here:

HandleEnum(Language.CSharp);
HandleEnum((Language)1); // 1 is CSharp

Either of these method calls is valid. Unfortunately, the following method calls are also valid:

HandleEnum((Language)100);
int someVar = 42;
HandleEnum((Language)someVar);

These method calls will also compile without errors, but odd behavior will result if the code in HandleEnum tries to use the value passed in to it (in this case, the value 100). In many cases, an exception will not even be thrown; HandleEnum just receives the value 100 as an argument, as if it were a legitimate value of the Language enumeration.

The CheckLanguageEnumValue method prevents this from happening by screening the argument for valid Language enumeration values. The following code shows the modified body of the HandleEnum method:

public static void HandleEnum(Language language)
{
    if (CheckLanguageEnumValue(language))
    {
        // Use language here
        Console.WriteLine($"{language} is an OK enum value");
    }
    else
    {
        // Deal with the invalid enum value here
        Console.WriteLine($"{language} is not an OK enum value");
    }
}

See Also

To test for a valid enumeration within an enumeration marked with the Flags attribute, see Recipe 3.10.

3.10 Using Enumerated Members in a Bit Mask

Problem

You need an enumeration of values to act as bit flags that can be ORed together to create a combination of values (flags) in the enumeration.

Solution

Mark the enumeration with the Flags attribute:

[Flags]
public enum RecycleItems
{
    None         = 0x00,
    Glass        = 0x01,
    AluminumCans = 0x02,
    MixedPaper   = 0x04,
    Newspaper    = 0x08
}

Combining elements of this enumeration is a simple matter of using the bitwise OR operator (|). For example:

RecycleItems items = RecycleItems.Glass | RecycleItems.Newspaper;

Discussion

Adding the Flags attribute to an enumeration marks this enumeration as individual bit flags that can be ORed together. Using an enumeration of flags is no different than using a regular enumeration type. Note that failing to mark an enumeration with the Flags attribute will not generate an exception or a compile-time error, even if the enumeration values are used as bit flags.

The addition of the Flags attribute provides you with two benefits. First, if the Flags attribute is placed on an enumeration, the ToString and ToString("G") methods return a string consisting of the name of the constant(s) separated by commas. Otherwise, these two methods return the numeric representation of the enumeration value. Note that the ToString("F") method returns a string consisting of the name of the constant(s) separated by commas, regardless of whether this enumeration is marked with the Flags attribute. The second benefit is that when you examine the code and encounter an enumeration, you can better determine the developer’s intention for this enumeration. If the developer explicitly defined it as containing bit flags (with the Flags attribute), you can use it as such.

An enumeration tagged with the Flags attribute can be viewed as a single value or as one or more values combined into a single enumeration value. If you need to accept multiple languages at a single time, you can write the following code:

RecycleItems items = RecycleItems.Glass | RecycleItems.Newspaper;

The variable items is now equal to the bit values of the two enumeration values ORed together. These values ORed together will equal 3, as shown here:

RecycleItems.Glass         0001
RecycleItems.AluminumCans  0010
ORed bit values            0011

The enumeration values were converted to binary and ORed together to get the binary value 0011, or 3 in base10. The compiler views this value both as two individual enumeration values (RecycleItems.Glass and RecycleItems.AluminumCans) ORed together or as a single value (3).

To determine if a single flag has been turned on in an enumeration variable, use the bitwise AND (&) operator, as follows:

RecycleItems items = RecycleItems.Glass | RecycleItems.Newspaper;
if((items & RecycleItems.Glass) == RecycleItems.Glass)
    Console.WriteLine("The enum contains the C# enumeration value");
else
    Console.WriteLine("The enum does NOT contain the C# value");

This code will display the text The enum contains the C# enumeration value. The ANDing of these two values will produce 0 if the variable items does not contain the value RecycleItems.Glass, or produce RecycleItems.Glass if items contains this enumeration value. Basically, ANDing these two values looks like this in binary:

RecycleItems.Glass | RecycleItems.AluminumCans 0011
RecycleItems.Glass                             0001
ANDed bit values                               0001

We will deal with this in more detail in Recipe 3.11.

In some cases, the enumeration can grow quite large. You can add many other recyclable items to this enumeration, as shown here:

[Flags]
public enum RecycleItems
{
    None         = 0x00,
    Glass        = 0x01,
    AluminumCans = 0x02,
    MixedPaper   = 0x04,
    Newspaper    = 0x08,
    TinCans      = 0x10,
    Cardboard    = 0x20,
    ClearPlastic = 0x40,
}

If you needed a RecycleItems enumeration value to represent all recyclable items, you would have to OR together each value of this enumeration:

RecycleItems items = RecycleItems.Glass | RecycleItems.AluminumCans |
                     RecycleItems.MixedPaper;

Instead of doing this, you can simply add a new value to this enumeration that includes all recyclable items:

[Flags]
public enum RecycleItems
{
    None            = 0x00,
    Glass           = 0x01,
    AluminumCans    = 0x02,
    MixedPaper      = 0x04,
    Newspaper       = 0x08,
    TinCans         = 0x10,
    Cardboard       = 0x20,
    ClearPlastic    = 0x40,
    All = (None | Glass | AluminumCans | MixedPaper | Newspaper | TinCans |
           Cardboard | ClearPlastic)
}

Now there is a single enumeration value, All, that encompasses every value of this enumeration. Notice that there are two methods of creating the All enumeration value. The second method is much easier to read. Regardless of which method you use, if individual language elements of the enumeration are added or deleted, you will have to modify the All value accordingly.

Note

You should provide a None value for all enums even where “none of the above” does not make sense, because it is always legal to assign literal 0 to an enum, and because enum variables, which begin their lives assigned to their default values, start as 0.

Similarly, you can also add values to capture specific subsets of enumeration values as follows:

[Flags]
enum Language
{
    CSharp = 0x0001, VBNET = 0x0002, VB6 = 0x0004, Cpp = 0x0008,
    CobolNET = 0x000F, FortranNET = 0x0010, JSharp = 0x0020,
    MSIL = 0x0080,
    All = (CSharp | VBNET | VB6 | Cpp | FortranNET | JSharp | MSIL),
    VBOnly = (VBNET | VB6),
    NonVB = (CSharp | Cpp | FortranNET | JSharp | MSIL)
}

Now you have two extra members in the enumerations—one that encompasses VB-only languages (Languages.VBNET and Languages.VB6) and one that encompasses non-VB languages.

3.11 Determining Whether One or More Enumeration Flags Are Set

Problem

You need to determine if a variable of an enumeration type, consisting of bit flags, contains one or more specific flags. For example, given the following enumeration Language:

[Flags]
enum Language
{
    CSharp = 0x0001, VBNET = 0x0002, VB6 = 0x0004, Cpp = 0x0008
}

determine, using Boolean logic, if the variable lang in the following line of code contains a language such as Language.CSharp and/or Language.Cpp:

Language lang = Language.CSharp | Language.VBNET;

Solution

To determine if a variable contains a single bit flag that is set, use the following conditional:

if((lang & Language.CSharp) == Language.CSharp)
{
    // Lang contains at least Language.CSharp.
}

To determine if a variable exclusively contains a single bit flag that is set, use the following conditional:

if(lang == Language.CSharp)
{
    // lang contains only the Language.CSharp
}

To determine if a variable contains a set of bit flags that are all set, use the following conditional:

if((lang & (Language.CSharp | Language.VBNET)) ==
  (Language.CSharp | Language.VBNET))
{
     // lang contains at least Language.CSharp and Language.VBNET.
}

To determine if a variable exclusively contains a set of bit flags that are all set, use the following conditional:

if((lang | (Language.CSharp | Language.VBNET)) ==
  (Language.CSharp | Language.VBNET))
{
     // lang contains only the Language.CSharp and Language.VBNET.
}

Discussion

When enumerations are used as bit flags and are marked with the Flags attribute, they usually will require some kind of conditional testing to be performed. This testing necessitates the use of the bitwise AND (&) and OR (|) operators.

To test for a variable having a specific bit flag set, use the following conditional statement:

if((lang & Language.CSharp) == Language.CSharp)

where lang is of the Language enumeration type.

The & operator is used with a bit mask to determine if a bit is set to 1. The result of ANDing two bits is 1 only when both bits are 1; otherwise, the result is 0. You can use this operation to determine if a specific bit flag is set to 1 in the number containing the individual bit flags. If you AND the variable lang with the specific bit flag you are testing for (in this case, Language.CSharp), you can extract that single specific bit flag. The expression (lang & Language.CSharp) is solved in the following manner if lang is equal to Language.CSharp:

Language.CSharp  0001
lang             0001
ANDed bit values 0001

If lang is equal to another value, such as Language.VBNET, the expression is solved as follows:

Language.CSharp  0001
lang             0010
ANDed bit values 0000

Notice that ANDing the bits together returns the value Language.CSharp in the first expression and 0x0000 in the second expression. Comparing this result to the value you are looking for (Language.CSharp) tells you whether that specific bit was turned on.

This method is great for checking specific bits, but what if you want to know whether only one specific bit is turned on (and all other bits turned off) or off (and all other bits turned on)? To test if only the Language.CSharp bit is turned on in the variable lang, you can use the following conditional statement:

if(lang == Language.CSharp)

If the variable lang contained only the value Language.CSharp, the expression using the OR operator would look like this:

lang = Language.CSharp;
if ((lang != 0) &&(Language.CSharp == (lang | Language.CSharp)))
{
    // CSharp is found using OR logic
}

Language.CSharp 0001
lang 0001
ORed bit values 0001

Now, add a language value or two to the variable lang and perform the same operation on lang:

lang = Language.CSharp | Language.VB6 | Language.Cpp;
if ((lang != 0) &&(Language.CSharp == (lang | Language.CSharp)))
{
    // CSharp is found using OR logic
}

Language.CSharp 0001
lang 1101
ORed bit values 1101

The first expression results in the same value as the one you are testing against. The second expression results in a much larger value than Language.CSharp. This indicates that the variable lang in the first expression contains only the value Language.CSharp, whereas the second expression contains other languages besides Language.CSharp (and may not contain Language.CSharp at all).

Using the OR version of this formula, you can test multiple bits to determine if they are on and all other bits are off, as shown in the following conditional statement:

if((lang != 0) && ((lang | (Language.CSharp | Language.VBNET)) ==
   (Language.CSharp | Language.VBNET)))

Notice that to test for more than one language, you simply OR the language values together. By switching the first | operator to an & operator, you can determine if at least these bits are turned on, as shown in the following conditional statement:

if((lang != 0) && ((lang & (Language.CSharp | Language.VBNET)) ==
   (Language.CSharp | Language.VBNET)))

When testing for multiple enumeration values, you may find it beneficial to add a value to your enumeration, which ORs together all the values you want to test for. If you wanted to test for all languages except Language.CSharp, your conditional statement(s) would grow quite large and unwieldy. To fix this, you add a value to the Language enumeration that ORs together all languages except Language.CSharp. The new enumeration looks like this:

[Flags]
enum Language
{
    CSharp = 0x0001, VBNET = 0x0002, VB6 = 0x0004, Cpp = 0x0008,
    AllLanguagesExceptCSharp = VBNET | VB6 | Cpp
}

and your conditional statement might look similar to the following:

if((lang != 0) && (lang | Language.AllLanguagesExceptCSharp) ==
    Language. AllLanguagesExceptCSharp)

This is quite a bit smaller, easier to manage, and easier to read.

Note

Use the AND operator when testing if one or more bits are set to 1. Use the OR operator when testing if one or more bits are set to 0.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.224.135