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.
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.
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 int
s does not provide. The CLR sees enumerations as members of their underlying types, so it is not type-safe for all languages.
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)); }
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(); }
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.
Recipe 3.2, and the “Convert.ToBase64CharArray Method” topic in the MSDN documentation.
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); } }
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.
Recipe 3.1, and the “Convert.FromBase64CharArray Method” topic in the MSDN documentation.
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
.
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);
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.
The “ASCIIEncoding Class” and “UnicodeEncoding Class” topics in the MSDN documentation.
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[]
.
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);
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.
The “ASCIIEncoding Class” and “UnicodeEncoding Class” topics in the MSDN documentation.
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! }
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.
The “Parse” and “TryParse” topics in the MSDN documentation.
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
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.
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.
The “Math Class” topic in the MSDN documentation.
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); }
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.
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.
The “Math Class” topic in the MSDN documentation.
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. }
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.
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.
The “Checked Keyword” and “Checked and Unchecked” topics in the MSDN documentation.
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; }
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"); } }
To test for a valid enumeration within an enumeration marked with the Flags
attribute, see Recipe 3.10.
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;
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.
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.
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;
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. }
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.
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
.