Chapter 9. Operator Overloading

Operator overloading is used to add implicit or explicit operator behavior to types. Primitive types, such as integers, floats, and doubles, implicitly support most operators, including mathematical and logical operators. Other commonly used types, such as strings, support a limited set of operators. This intrinsic integration of standard operators in primitive types makes using built-in types convenient. You might want to use a user-defined type just as conveniently. Unfortunately, the C# compiler does not know how to apply standard operators to user-defined classes. For example, what does zclass+zclass mean? Operator overloading teaches the compiler how to interpret standard operators in the context of a user-defined type. You can overload more than one operator in a user-defined type and create the same convenience as is available when manipulating a primitive type.

User-defined types that represent numerical constructs benefit the most from operator overloading. Fractions, complex numbers, summations, the hexadecimal numerical system, matrixes, and other similar types have behavior that might be expressed with operator overloading. For example, if your application deals with matrixes, then an overloaded plus operator for adding two matrixes together would be beneficial.

Operator overloading also is useful for casting one type to another. Conversion operators are available to convert or cast a user-defined type to some other type. With few exceptions, you can cast between primitive types. For example, you can implicitly cast from a short value to a long one. You also can cast explicitly from a long value to a short one, and cast between related user-defined types, which is the basis of polymorphism. However, what does it mean to cast a ZClass instance to an int value? ZClass is not a primitive and is unrelated to an int. The compiler must be taught how to make this conversion. Developers create conversion operators to instruct the compiler in the conversions of user-defined types to unrelated types.

Operator overloading should be intuitive and is never required. Do not stray from the underlying intent of the operator. For example, the plus operator should perform some type of addition. System.String overloads operator+ to concatenate strings. Concatenation and addition are similar concepts. If the implementation of operator+ for a string type truncated characters instead, that would be nonintuitive and confusing. Operator overloading can be overused and is not appropriate in all circumstances. Overloading operator+ is a reasonable addition to a string class. However, overloading operator-, although possible, would be nonsensical in a string class. Remember, the overloaded operator must be intuitive to everyone, not just to the developer. Confer with peers and validate your perception of intuitive behavior. An alternative to operator overloading, and sometimes a better solution, is simply implementing a named method, such as an Add method instead of an overloading operator+ and a Subtract method instead of overloading an operator-. There is the added benefit that named methods are called explicitly, whereas operator functions are called implicitly. Because overloaded operators are called implicitly, their use sometimes hides what is occurring. Explicit method calls avert the type of surprises that drive maintenance programmers crazy.

Mathematical and Logical Operators

Mathematical and logical operators can be unary, binary, or ternary. Most binary and unary operators can be overloaded in a user-defined type. The sole ternary operator, which is the conditional operator, cannot be overloaded. This is the list of operators, including mathematical and logical operators, that cannot be overloaded in a user-defined type:

  • Dot operator (identifier.member)

  • Call operator (methodname())

  • Assignment and compound assignment operators (=, +=, /=, %=, and so on)

  • Conditional operators (&&, ||, and the ternary ? : operator)

  • The checked and unchecked operators

  • The new operator

  • The typeof operator

  • The as and is operators

  • The array operator ([])

  • Pointer operators

The compound assignment operators, such as += and /=, are implicitly overloaded by overloading the derivative operator. For example, the += and /= operators are implicitly overloaded by overloading the + and / operators, respectively.

Operator overloading cannot define new operators. For example, you could not define a ^^ operator, which does not otherwise exist.

Many developers are familiar with operator overloading in C++. There are some notable differences between operator overloading in the C++ and C# languages. This may be an issue when porting code from C++ to C#. For example in C++, you can overload the assignment, new, and array operators. This is not allowed in C# for the following reasons:

  • The Garbage Collector (GC) is responsible for managing dynamic memory. For that reason, the new operator cannot be overloaded in managed code.

  • The array operator is commonly overloaded in C++ to create a secure array, where fencepost errors are usually checked. Fencepost errors are detected automatically by the CLR, which eliminates one of the primary reasons to overload the array operator.

  • Instead of overloading the assignment operator as in C++, implement the ICloneable interface in C#.

For these reasons, porting the code for operator overloading from C++ to C# might not be trivial.

Operator overloading defines the behavior of an operator in the context of a type. The operator then can be used in an expression in the normal manner. You cannot change the syntax, precedence, or associativity of an operator through operator overloading. In other words, overloading an operator does not change the core principles of using the operator. For example, assume that the division operator is overloaded in a user-defined type. The syntax for employing the division operator remains obj1 / obj2. Precedence is also preserved. In ++obj1 + obj2 / obj3 - obj4, the division operation is evaluated before a plus or minus operator, but after the increment operator. Multiple division operations, as in obj1 / obj2 + obj3 / obj4 are evaluated from left to right, which maintains the normal associativity.

Implementation

Overloaded operators are implemented as static and public functions. Other modifiers, such as virtual and sealed, are not allowed. As an overloaded function, the unary operator has a single operand, whereas the binary operator has two operands. Parameters of an overloaded operator cannot be ref or out.

Here is the signature for overloading a unary or binary operator method:

public static type operator unaryoperator(type operand)

public static type operator binaryoperator(type lhsoperand, type rhsoperand)

When overloading a unary operator, the single operand must be of the same type as the containing class. When overloading a binary operator, at least one of the parameters should be the same type as the containing type. The first and second parameter in an overloaded binary operator represent the left-hand side (lhs) and right-hand side (rhs) of the related expression, respectively. In the expression obj + 5, the object instance obj is passed as lhsoperand to the operator+ method, while the integer value 5 is passed as rhsoperand. Therefore an operator+ overload must exist for the obj type with those parameter types. Operator overloading must return some type. The return type cannot be void.

The following is an example of the implementation and use of an overloaded operator. ZClass is a simple wrapper for two integer fields. The operator+ method is overloaded to add two ZClass instances. Because the operator+ method is part of the class, it has access to the private members of that class. This is convenient when accessing fields of that type in the overloaded operator method. In this code, operator+ is called implicitly in the expression: obj1 + obj2. You cannot call an overloaded operator-method explicitly:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            ZClass obj1 = new ZClass(5,10);
            ZClass obj2 = new ZClass(15,20);
            ZClass obj3 = obj1 + obj2;
            Console.WriteLine(obj3.Total);
        }
    }

    public class ZClass {

        public ZClass(int _fielda, int _fieldb) {
            fielda = _fielda;
            fieldb = _fieldb;
        }

        public static ZClass operator+(ZClass lhs, ZClass rhs) {
            return new ZClass(lhs.fielda + rhs.fielda,
                lhs.fieldb + rhs.fieldb);
        }

        public int Total {
            get {
                return fielda + fieldb;
            }
        }

        protected int fielda, fieldb;
    }
}

Operator methods are inherited in a derived class. Remember that the type of one of the parameters is the containing type. For an inherited overloaded operator method, the type of this parameter is the base type. A derived instance will be passed as the base type when the inherited operator method is called. The return type also might be the base type. Of course, you could simply implement operator overloading again in the derived class and override the inherited behavior. The implementation in the derived class would replace the base overloaded operator method.

In the following code, ZClass implements the operator+ method. YClass derives from ZClass and inherits operator+. It also inherits the fielda and fieldb fields. (ZClass.operator+ was shown in the previous code.) YClass also implements an operator- method. In Main, both the operator+ (inherited) and operator- methods are used. Calling the operator+ method on YClass parameters returns an instance of ZClass, which is the base type:

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            YClass obj1 = new YClass(5,10);
            YClass obj2 = new YClass(15,20);
            ZClass obj3 = obj1 + obj2;
            Console.WriteLine(obj3.Total);
            YClass obj4 = obj1 - obj2;
            Console.WriteLine(obj4.Total);
        }
    }

    // Partial listing

    public class YClass : ZClass {

        public YClass(int _fielda, int _fieldb) :
            base(_fielda, _fieldb) {
        }

        public static YClass operator-(YClass lhs, YClass rhs) {
            return new YClass(lhs.fielda - rhs.fielda,
               lhs.fieldb - rhs.fieldb);
        }
    }
}

You can overload an already overloaded operator method. The general rules of overloading a function apply. Each overloaded operator method must have a unique signature. Operators typically are overloaded to use the containing type as the lhs operand, as the rhs operand, or as both.

In the following code, operator+ is overloaded three times:

public static ZClass operator+(ZClass lhs, ZClass rhs) {
    return new ZClass(lhs.fielda + rhs.fielda,
        lhs.fieldb + rhs.fieldb);
}

public static ZClass operator+(ZClass lhs, int rhs) {
    return new ZClass(lhs.fielda + rhs,
        lhs.fieldb + rhs);
}


public static ZClass operator+(int lhs, ZClass rhs) {
    return new ZClass(lhs + rhs.fielda,
        lhs + rhs.fieldb);
}

All three operator methods are used in the following code:

obj3 = obj1 + obj2;
obj1 = obj1 + 10;
obj2 = 20 + obj2;

Operator overloading is not included in the Common Language Specification (CLS) and therefore this feature is not automatically included in all managed languages. For this reason, it is good policy to implement a parallel named method for each operator method. This provides an explicit alternative to implicit operator overloading. The named method should call the corresponding operator method, as demonstrated in the following code:

public class ZClass {

    public ZClass(int _fielda, int _fieldb) {
        fielda = _fielda;
        fieldb = _fieldb;
    }

    public static ZClass operator+(ZClass lhs, ZClass rhs) {
        return new ZClass(lhs.fielda + rhs.fielda,
            lhs.fieldb + rhs.fieldb);
    }

    public ZClass Add(ZClass rhs) {
        return this + rhs;
    }

    public int Total {
        get {
            return fielda + fieldb;
        }
    }

    protected int fielda, fieldb;
}

The semantics of overloading some mathematical and logical operators are unique. For these operators, which are described later in this chapter, special rules must be followed when implementing operator overloading.

Increment and Decrement Operators

The Increment (++) and Decrement (--) operators have special semantics. These are unary operators, where the operand type and the return type must be the containing class or a derivative. The Increment and Decrement operators accept a single parameter, which represents the current object. You must update this parameter and return this object in both the Increment and Decrement operators. Remember that overloaded operators should preserve the underlying meaning of the operator. The Increment and Decrement operators normally alter the current object. Finally, overloading the Increment or Decrement operator revises both the prefix and postfix usage of the operator.

Here is sample code showing an overloading of both the Increment and Decrement operators:

public class ZClass {

    public ZClass(int _fielda, int _fieldb) {
        fielda = _fielda;
        fieldb = _fieldb;
    }

    public static ZClass operator++(ZClass curr) {
        ++curr.fielda;
        ++curr.fieldb;
        return curr;
    }


    public static ZClass operator--(ZClass curr) {
        --curr.fielda;
        --curr.fieldb;
        return curr;
    }

    public int Total {
        get {
            return fielda + fieldb;
        }
    }

    public int fielda, fieldb;
}

LeftShift and RightShift Operators

The LeftShift and RightShift operators normally perform a bitwise shift. Both are binary operators. When overloading these operators, the first operand must be the same type as the containing class. The second operand must be an int, which specifies the amount of the shift. The return type can be anything but not void. The LeftShift and RightShift operators are not paired operator methods. You can implement them independently of each other. However, I would recommend implementing both because the methods are logically related:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            ZClass obj1 = new ZClass(5,10);
            ZClass obj2 = obj1 << 2;
            Console.WriteLine(obj2.Total);
        }
    }

    public class ZClass {

        public ZClass(int _fielda, int _fieldb) {
            fielda = _fielda;
            fieldb = _fieldb;
        }


        public static ZClass operator<<(ZClass curr, int shift) {
            int newa = curr.fielda << shift;
            int newb = curr.fieldb << shift;
            return new ZClass(newa, newb);
        }


        public static ZClass operator>>(ZClass curr, int shift) {
            int newa = curr.fielda >> shift;
            int newb = curr.fieldb >> shift;
            return new ZClass(newa, newb);
        }

        public int Total {
            get {
                return fielda + fieldb;
            }
        }

        public int fielda, fieldb;
    }
}

Operator True and Operator False

True or false logical operators are used in conditional expressions and sometimes in assignments. They are paired operator methods, which require both functions of the pair to be implemented. If either the operator true or operator false method is overloaded, both must be implemented. The operator methods for true or false are unary operators, where the current object is passed as the operand. The return type of the function should be bool. Add operator true and operator false methods to classes that have a false or true representation that can be interpreted from the state of the object. When the target object is used within a Boolean expression, the operator true method is called. The operator false method is called in other circumstances, such as in an AND (&&) expression. Overloading the && operator is discussed later in this chapter.

Here is sample code of an operator true and operator false method pair. The operator true method is used in the Main method within the if statement:

using System;

namespace Switch {
    class Program {
        static void Main(string[] args) {
            LightSwitch light = new LightSwitch();
            light.wattage = 60;
            light.on = true;
            if (light) {
                Console.Write("Light is on.");
            }
            else {
                Console.WriteLine("Light is off.");
            }
        }
    }

    class LightSwitch {

        public int wattage
        {
            set; get;
        }

        public bool on
        {
            set; get;
        }

        public static bool operator true(LightSwitch item)
        {
            return item.on;
        }

        public static bool operator false(LightSwitch item)
        {
            return !item.on;
        }
    }
}

Paired Operators

In addition to operator true and operator false, the relational operators are paired operators. Paired operators enforce a practical policy—two related operators both should be overloaded to maintain consistency. For example, because they are logically related, you should overload both the operator== and operator!= methods. Here is the complete list of paired relational operators:

  • operator== and operator!=

  • operator| and operator&

  • operator|| and operator&&

Operator== and Operator!=

The overloaded operator== method should call the Equals method, which ensures that the operator== and the Equals methods exhibit consistent behavior. If not, the definition of equality for an instance will vary based on circumstances, which could be confusing. The operator== and operator!= methods are paired methods. When overloading these operators, you also must implement the Equals method in the same class, as demonstrated in the following code:

using System;

namespace Wallet {
    class Program {
        static void Main(string[] args) {
            Wallet bob = new Wallet();
            bob.ten = 5;
            bob.five = 1;
            Wallet sally = new Wallet();
            sally.fifty = 1;
            sally.dollar = 5;
            Console.WriteLine("Bob {0}: Sally {1}
",
                bob.Total(), sally.Total());
            if (bob == sally) {
                Console.WriteLine(
                    "They have the same amount of money.");
            }
            else {
                Console.WriteLine(
                    "They do not have the same amount of money.");
            }
        }
    }

    class Wallet {
        public uint dollar
        {
            set;
            get;
        }

        public uint five
        {
            set;
            get;
        }

        public uint ten
        {
            set;
            get;
        }

        public uint twenty
        {
            set;
            get;
        }

        public uint fifty
        {
            set;
            get;
        }

        public static bool operator ==(Wallet lhs, Wallet rhs) {
            return lhs.Equals(rhs);
        }

        public static bool operator !=(Wallet lhs, Wallet rhs) {
            return !lhs.Equals(rhs);
        }

        public override bool Equals(object o) {
            return this.GetHashCode() == o.GetHashCode();
        }

        public override int GetHashCode() {
            return (int) Total();
        }

        public uint Total() {
            return dollar +
                (five * 5) +
                (ten * 10) +
                (twenty * 20) +
                (fifty * 50);
        }
    }
}

Operator| and Operator&

You also can overload operator | and operator& in the context of a user-defined class. The operator| and operator& methods already are overloaded for integral or Boolean operands. These operators perform a bitwise operation on integral values; operator| performs a bitwise OR, while operator& does a bitwise AND. These operators perform a logical comparison when used with Boolean values; operator| does a logical OR, while operator& conducts a logical AND. The Boolean overload of the operator| and operator& functions will not short-circuit. Both the left and right operands always will be evaluated.

The following code introduces the Membership class, which naturally maintains membership lists. The operator| method is overloaded to combine membership lists. The operator& method is overloaded in the same class to return the intersection of two lists. This usage adheres to the general concept of these operators, which is always important when overloading operators.

Here is the code:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;

namespace ConsoleApplication2 {
    class Program {
        static void Main(string[] args) {
            Membership store1 = new Membership();
            store1.Add("bob");
            store1.Add("sally");

            Membership store2 = new Membership();
            store2.Add("sally");
            store2.Add("fred");

            Membership store3 = store1 & store2;
            Membership store4 = store1 | store2;

            foreach (string name in store3.GetNames()) {
                Console.Write(name + " ");
            }
            Console.WriteLine("
end of list 1
");

            foreach (string name in store4.GetNames()) {
                Console.Write(name + " ");
            }
            Console.WriteLine("
end of list 2.");
        }
    }

    class Membership
    {

        private List<string> members =
            new List<string>();

        public void Add(string name) {
            members.Add(name);
        }

        public List<string> Detach() {
            return members.ToList();
        }

        public void Attach(List<string> newlist) {
            members = newlist;
        }

        public string[] GetNames() {
            return members.ToArray();
        }

        public static Membership operator |(Membership m1, Membership m2) {
            Membership obj = new Membership();
            obj.Attach(m1.Detach());

            string[] names = m1.GetNames();
            foreach (string name in m2.GetNames()) {
                if (!names.Contains(name)) {
                    obj.Add(name);
                }
            }
            return obj;
        }

        public static Membership operator &(Membership m1, Membership m2) {
            Membership obj = new Membership();
            List<string> names = m1.Detach();
            foreach (string name in m2.Detach()) {
                if (names.Contains(name)) {
                    obj.Add(name);
                }
            }
            return obj;
        }
    }
}

Operator|| and Operator&&

Operator|| and operator&& cannot be overloaded directly. These are the logical AND and OR operators, respectively. The operator|, operator&, operator true, and operator false methods combine to overload operator|| and operator&& functionally.

The operator&& or operator|| methods can short-circuit and partially evaluate. If the lhs of an && expression is false, the entire expression is false. This means the rhs can be ignored. For || expressions, the entire expression is true if the lhs is true. When that occurs, the rhs of the || expression can be ignored. The possibility of short-circuiting complicates the understanding of how the compiler overloads the && and || operators.

Following are the steps the compiler follows for evaluating operator && for the expression lhs && rhs. Let us assume that lhs and rhs are the same type as the containing class. This is also the case for operator& and operator|.

  1. The operator false(lhs) method is called.

  2. If operator false returns true, the expression evaluates to the operator true(lhs) method.

  3. If operator false returns false, the overloaded operator& (lhs, rhs) is called, which returns result. The expression evaluates to operator true(result).

The compiler executes the following steps for evaluating the operator||, where the expression is lhs || rhs. (The class for lhs and rhs must be the containing class.)

  1. The operator true(lhs) method is called.

  2. If operator true returns true, the expression evaluates to the operator true(lhs) method.

  3. If operator true returns false, the overloaded operator |(lhs, rhs) is called, which returns result. The expression evaluates to operator true(result).

The following code is stubbed to document the sequence of calls for operator&& and operator||. This code confirms the order of execution for a user-defined operator&& or operator||:

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            ZClass obj1 = new ZClass();
            obj1.tag = "lhs";

            // change obj1.state to test scenarios

            obj1.state = false;

            ZClass obj2 = new ZClass();
            obj2.tag = "rhs";

            obj2.state = false;
            if (obj1 || obj2) {
                Console.WriteLine("ojb1 || obj2 is true.");
            }
            else {
                Console.WriteLine("ojb1 || obj2 is false.");
            }
        }
    }

    class ZClass {
        public static bool operator false(ZClass obj) {
            Console.WriteLine("operator false for " + obj.tag);
            return obj.state;
        }

        public static bool operator true(ZClass obj) {
            Console.WriteLine("operator true for " + obj.tag);
            return obj.state;
        }


        public static ZClass operator &(ZClass obj1, ZClass obj2) {
            Console.WriteLine("operator & : {0} {1} ",
                obj1.tag, obj2.tag);
            ZClass obj = new ZClass();
            obj.tag = "[obj from operator &]";
            return obj;
        }

        public static ZClass operator |(ZClass obj1, ZClass obj2) {
            Console.WriteLine("operator | : {0} {1} ",
                obj1.tag, obj2.tag);
            ZClass obj = new
            ZClass(); obj.tag = "[obj from operator |]";
            return obj;
        }

        public string tag = "";
        public bool state = false;
    }
}
..................Content has been hidden....................

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