Что такое перегрузка в c#. Операции. Перегрузка операций. Вызов базовых версий методов

Вайбер на компьютер 13.04.2019

Последнее обновление: 12.08.2018

Наряду с методами мы можем также перегружать операторы. Например, пусть у нас есть следующий класс Counter:

Class Counter { public int Value { get; set; } }

Данный класс представляет некоторый счетчик, значение которого хранится в свойстве Value.

И допустим, у нас есть два объекта класса Counter - два счетчика, которые мы хотим сравнивать или складывать на основании их свойства Value, используя стандартные операции сравнения и сложения:

Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Counter c3 = c1 + c2;

Но на данный момент ни операция сравнения, ни операция сложения для объектов Counter не доступны. Эти операции могут использоваться для ряда примитивных типов. Например, по умолчанию мы можем складывать числовые значения, но как складывать объекты комплексных типов - классов и структур компилятор не знает. И для этого нам надо выполнить перегрузку нужных нам операторов.

Перегрузка операторов заключается в определении в классе, для объектов которого мы хотим определить оператор, специального метода:

Public static возвращаемый_тип operator оператор(параметры) { }

Этот метод должен иметь модификаторы public static , так как перегружаемый оператор будет использоваться для всех объектов данного класса. Далее идет название возвращаемого типа. Возвращаемый тип представляет тот тип, объекты которого мы хотим получить. К примеру, в результате сложения двух объектов Counter мы ожидаем получить новый объект Counter. А в результате сравнения двух мы хотим получить объект типа bool, который указывает истинно ли условное выражение или ложно. Но в зависимости от задачи возвращаемые типы могут быть любыми.

Затем вместо названия метода идет ключевое слово operator и собственно сам оператор. И далее в скобках перечисляются параметры. Бинарные операторы принимают два параметра, унарные - один параметр. И в любом случае один из параметров должен представлять тот тип - класс или структуру, в котором определяется оператор.

Например, перегрузим ряд операторов для класса Counter:

Class Counter { public int Value { get; set; } public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; } public static bool operator >(Counter c1, Counter c2) { return c1.Value > c2.Value; } public static bool operator <(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Поскольку все перегруженные операторы - бинарные - то есть проводятся над двумя объектами, то для каждой перегрузки предусмотрено по два параметра.

Так как в случае с операцией сложения мы хотим сложить два объекта класса Counter, то оператор принимает два объекта этого класса. И так как мы хотим в результате сложения получить новый объект Counter, то данный класс также используется в качестве возвращаемого типа. Все действия этого оператора сводятся к созданию, нового объекта, свойство Value которого объединяет значения свойства Value обоих параметров:

Public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; }

Также переопределены две операции сравнения. Если мы переопределяем одну из этих операций сравнения, то мы также должны переопределить вторую из этих операций. Сами операторы сравнения сравнивают значения свойств Value и в зависимости от результата сравнения возвращают либо true, либо false.

Теперь используем перегруженные операторы в программе:

Static void Main(string args) { Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); }

Стоит отметить, что так как по сути определение оператора представляет собой метод, то этот метод мы также можем перегрузить, то есть создать для него еще одну версию. Например, добавим в класс Counter еще один оператор:

Public static int operator +(Counter c1, int val) { return c1.Value + val; }

Данный метод складывает значение свойства Value и некоторое число, возвращая их сумму. И также мы можем применить этот оператор:

Counter c1 = new Counter { Value = 23 }; int d = c1 + 27; // 50 Console.WriteLine(d);

Следует учитывать, что при перегрузке не должны изменяться те объекты, которые передаются в оператор через параметры. Например, мы можем определить для класса Counter оператор инкремента:

Public static Counter operator ++(Counter c1) { c1.Value += 10; return c1; }

Поскольку оператор унарный, он принимает только один параметр - объект того класса, в котором данный оператор определен. Но это неправильное определение инкремента, так как оператор не должен менять значения своих параметров.

И более корректная перегрузка оператора инкремента будет выглядеть так:

Public static Counter operator ++(Counter c1) { return new Counter { Value = c1.Value + 10 }; }

То есть возвращается новый объект, который содержит в свойстве Value инкрементированное значение.

При этом нам не надо определять отдельно операторы для префиксного и для постфиксного инкремента (а также декремента), так как одна реализация будет работать в обоих случаях.

Например, используем операцию префиксного инкремента:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(++counter).Value}"); // 20 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Теперь используем постфиксный инкремент:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(counter++).Value}"); // 10 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Также стоит отметить, что мы можем переопределить операторы true и false . Например, определим их в классе Counter:

Class Counter { public int Value { get; set; } public static bool operator true(Counter c1) { return c1.Value != 0; } public static bool operator false(Counter c1) { return c1.Value == 0; } // остальное содержимое класса }

Эти операторы перегружаются, когда мы хотим использовать объект типа в качестве условия. Например:

Counter counter = new Counter() { Value = 0 }; if (counter) Console.WriteLine(true); else Console.WriteLine(false);

При перегрузке операторов надо учитывать, что не все операторы можно перегрузить. В частности, мы можем перегрузить следующие операторы:

    унарные операторы +, -, !, ~, ++, --

    бинарные операторы +, -, *, /, %

    операции сравнения ==, !=, <, >, <=, >=

    логические операторы &&, ||

    операторы присваивания +=, -=, *=, /=, %=

И есть ряд операторов, которые нельзя перегрузить, например, операцию равенства = или тернарный оператор?: , а также ряд других.

Полный список перегружаемых операторов можно найти в документации msdn

При перегрузке операторов также следует помнить, что мы не можем изменить приоритет оператора или его ассоциативность, мы не можем создать новый оператор или изменить логику операторов в типах, который есть по умолчанию в.NET.

Последнее обновление: 13.07.2018

Иногда возникает необходимость создать один и тот же метод, но с разным набором параметров. И в зависимости от имеющихся параметров применять определенную версию метода. Такая возможность еще называется перегрузкой методов (method overloading).

И в языке C# мы можем создавать в классе несколько методов с одним и тем же именем, но разной сигнатурой. Что такое сигнатура? Сигнатура складывается из следующих аспектов:

    Имя метода

    Количество параметров

    Типы параметров

    Порядок параметров

    Модификаторы параметров

Но названия параметров в сигнатуру НЕ входят. Например, возьмем следующий метод:

Public int Sum(int x, int y) { return x + y; }

У данного метода сигнатура будет выглядеть так: Sum(int, int)

И перегрузка метода как раз заключается в том, что методы имеют разную сигнатуру, в которой совпадает только название метода. То есть методы должны отличаться по:

    Количеству параметров

    Типу параметров

    Порядку параметров

    Модификаторам параметров

Например, пусть у нас есть следующий класс:

Class Calculator { public void Add(int a, int b) { int result = a + b; Console.WriteLine($"Result is {result}"); } public void Add(int a, int b, int c) { int result = a + b + c; Console.WriteLine($"Result is {result}"); } public int Add(int a, int b, int c, int d) { int result = a + b + c + d; Console.WriteLine($"Result is {result}"); return result; } public void Add(double a, double b) { double result = a + b; Console.WriteLine($"Result is {result}"); } }

Здесь представлены четыре разных версии метода Add, то есть определены четыре перегрузки данного метода.

Первые три версии метода отличаются по количеству параметров. Четвертая версия совпадает с первой по количеству параметров, но отличается по их типу. При этом достаточно, чтобы хотя бы один параметр отличался по типу. Поэтому это тоже допустимая перегрузка метода Add.

То есть мы можем представить сигнатуры данных методов следующим образом:

Add(int, int) Add(int, int, int) Add(int, int, int, int) Add(double, double)

После определения перегруженных версий мы можем использовать их в программе:

Class Program { static void Main(string args) { Calculator calc = new Calculator(); calc.Add(1, 2); // 3 calc.Add(1, 2, 3); // 6 calc.Add(1, 2, 3, 4); // 10 calc.Add(1.4, 2.5); // 3.9 Console.ReadKey(); } }

Консольный вывод:

Result is 3 Result is 6 Result is 10 Result is 3.9

Также перегружаемые методы могут отличаться по используемым модификаторам. Например:

Void Increment(ref int val) { val++; Console.WriteLine(val); } void Increment(int val) { val++; Console.WriteLine(val); }

В данном случае обе версии метода Increment имеют одинаковый набор параметров одинакового типа, однако в первом случае параметр имеет модификатор ref. Поэтому обе версии метода будут корректными перегрузками метода Increment.

А отличие методов по возвращаемому типу или по имени параметров не является основанием для перегрузки. Например, возьмем следующий набор методов:

Int Sum(int x, int y) { return x + y; } int Sum(int number1, int number2) { return x + y; } void Sum(int x, int y) { Console.WriteLine(x + y); }

Сигнатура у всех этих методов будет совпадать:

Sum(int, int)

Поэтому данный набор методов не представляет корректные перегрузки метода Sum и работать не будет.

Основы перегрузки операторов

В C#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму:

// Операция + с целыми. int а = 100; int b = 240; int с = а + b; //с теперь равно 340

Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

// Операция + со строками. string si = "Hello"; string s2 = " world!"; string s3 = si + s2; // s3 теперь содержит "Hello world!"

По сути, функциональность операции + уникальным образом базируются на представленных типах данных (строках или целых в данном случае). Когда операция + применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк.

Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

Операция C# Возможность перегрузки
+, -, !, ++, --, true, false Этот набор унарных операций может быть перегружен
+, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
==, !=, <, >, <=, >= Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки "подобных" операций (т.е. < и >, <= и >=, == и!=)
Операция не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
() Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово operator , определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса. Существуют две формы операторных методов (operator): одна - для унарных операторов, другая - для бинарных. Ниже приведена общая форма для каждой разновидности этих методов:

// Общая форма перегрузки унарного оператора. public static возвращаемый_тип operator op(тип_параметра операнд) { // операции } // Общая форма перегрузки бинарного оператора. public static возвращаемый_тип operator op(тип_параметра1 операнд1, тип_параметра2 операнд2) { // операции }

Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2 . Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа - public и static.

Перегрузка бинарных операторов

Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); } } }

Перегрузка унарных операторов

Унарные операторы перегружаются таким же образом, как и бинарные. Главное отличие заключается, конечно, в том, что у них имеется лишь один операнд. Давайте модернизируем предыдущий пример, дополнив перегрузки операций ++, --, -:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } // Перегружаем унарный оператор - public static MyArr operator -(MyArr obj1) { MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; } // Перегружаем унарный оператор ++ public static MyArr operator ++(MyArr obj1) { obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; } // Перегружаем унарный оператор -- public static MyArr operator --(MyArr obj1) { obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; return obj1; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2--; Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); } } }

Перекрытие методов.

Пример перекрытия метода в C#:

Public class myClass { // виртуальный метод public virtual string ToString() { return ""; } } public class myDelivierClass: myClass { // переопределенный, перекрытый метод public override string ToString() { return "new return"; } }

Сокрытие методов

Даже если в базовом классе метод не был объявлен виртуальным, в производном классе все равно можно объявить другой метод с такой же сигнатурой. Новый метод однако не перекроет метод базового класса. Напротив, говорят, что он скроет метод базового класса. При этом компилятор, решая, какой метод вызвать, всегда будет рассматривать тип данных, на который указывает переменная, как тип данных заданный при ее объявлении!

Скрытие (переопределение) элементов класса – это явное описание в классе-наследнике с новыми характеристиками уже существующих элементов из наследуемого класса.

Public class MyClass { public int MyNumber; } public class OurClass: MyClass { public double MyNumber; }

Переопределено поле MyNumber (в родительском классе имело целочисельный тип, а в производном – дробный). Поле целого типа скрыто новым описанием. Если бы поле MyNumber не было переопределено, то в классе OurClass (производный) по умолчанию было бы доступен только родительское поле MyNumber типа int. А выражение: oc.MyNumber = 5.5; вызывало ошибку типов.

Скрытие поля класса может произойти по ошибке, например программист случайно вводит название поля, которое уже существует в одном из классов, вышестоящих по цепочке наследования.

Если метод скрывает базового класса, то к его определению необходимо добавить ключевое слово new. Этот модификатор подскажет компилятору, что программист знает о факте сокрытия!

Public class Customer { public string GetFunnyString() { return "Cusomer funny!"; } } ... public class Nevermore60Customer: Customer { public new string GetFunnyString() { return "Nevermore60 funny!”; } } ... Customer Cust1; Nevermore60Customer Cust2; Cust1 = new Customer(); Console.WriteLine(Cust1.GetFunnyString()); Cust1 = new Nevermore60Customer(); Console.WriteLine(Cust1.GetFunnyString()); Cust2 = new Nevermore60Customer(); Console.WriteLine(Cust2.GetFunnyString()); ... } Результат: Cusomer funny! Cusomer funny! Nevermore60 funny!

В большинстве случаев лучше перекрыть (override ), а не скрыть методы, так как сокрытие методов представляет собой серьезную опасность того, что для данного экземпляра класса будет вызван «неправильный» метод.

Если класс определен как abstract , сделать его экземпляр невозможно. Если метод объявлен с модификатором abstract то этот метод будет рассматриваться как виртуальный и что вы не реализуете этот метод в классе в расчета на то, что он будет перекрыт в производных классах.

Если хотя бы один метод в классе абстрактный, то и весь класс должен быть абстрактным! Кроме того, любой класс, порожденный от абстрактного должен перекрыть его методы.

Public abstract class Customer { public abstract int MyAbstractMethod(); // тело отсуствует... public class Nevermore60Customer: Customer { public override int MyAbstractMethod() { return 0; } } }

Запечатанные (sealed) классы и методы можно рассматривать как противоположность абстрактным классам и методам. Объявление класса или метода запечатанным означает что произвести наследование и перекрытие не возможно.

Sealed class FinalClass // наследовать не возможно... public class myClass { // перекрытие в дальнейшем не озможно public sealed override FinalMethod() } ...

Вызов базовых версий методов

Ключевое слово base явно указывает компилятору, что происходит обращение к методу базового класса.

Public class CustomerAccount { public virtual decimal CalculatePrice (CustomerAccount account) { // ... return 2000M; } } public class GoldAccount: CustomerAccount { public override decimal CalculatePrice(CustomerAccount account) { return base.CalculatePrice(account) * 0.9M; } }

Индексаторы, операции, свойства можно делать виртуальными и перекрывать их по желанию.

Поля не могут быть объявлены виртуальными или перекрытыми. Однако можно скрыть базовую версию поля, объявив другое поле с тем же именем в производном классе с модификатором (new). Получить доступ к базовому полю можно через base.[имя_поля].

Статические методы не могут быть объявлены виртуальными, но могут быть скрытыми (new). Не имеет смысла объявлять виртуальным статический член вообще!

Перегрузка методов

Перегрузка методов в C# означает, что в классе можно определить несколько методов с одним и тем же именем при условии, что эти методы получают разное число параметров. Не следует путать перегрузку методов с перекрытием методов. Перегрузка методов не имеет ничего общего с наследованием или виртуальными методами.

C# не разрешает использование значений по умолчанию для параметров.

Пример перегрузки методов: Console.WriteLine();



Рекомендуем почитать

Наверх