Процедуры и функции - методы класса
Функции с побочным эффектом
Функция называется функцией с побочным эффектом, если помимо результата, вычисляемого функцией и возвращаемого ей в операторе return, она имеет выходные аргументы с ключевыми словами ref и out. В языках C/C++ функции с побочным эффектом применяются сплошь и рядом. Хороший стиль ОО-программирования не рекомендует применение таких функций. Выражения, использующие функции с побочным эффектом, могут потерять свои прекрасные свойства, присущие им в математике. Если F(a) - функция с побочным эффектом, то a + F(a) может быть не равно F(a) + a, так что теряется коммутативность операции сложения.
Примером такой функции является функция F, приведенная выше. Вот тест, демонстрирующий потерю коммутативности сложения при работе с этой функцией:
/// <summary> /// тестирование побочного эффекта /// </summary> public void TestSideEffect() { int a = 0, b=0, c=0; a = 1; b = a + F(ref a); a = 1; c = F(ref a) + a; Console.WriteLine("a={0}, b={1}, c={2}",a, b, c); }
На рис. 5.2 показаны результаты работы этого метода.
Обратите внимание на полезность указания ключевого слова ref в момент вызова. Его появление хоть как-то оправдывает некоммутативность сложения.
Напомню, что и выражения с побочным эффектом также приводят к потере коммутативности сложения и умножения. Выражение x + ++x не эквивалентно выражению ++x + x.
Методы. Перегрузка
Должно ли быть уникальным имя метода в классе? Нет, это не требуется. Более того, проектирование методов с одним и тем же именем является частью стиля программирования на С++ и стиля C#. Существование в классе методов с одним и тем же именем называется перегрузкой, а сами одноименные методы называются перегруженными.
Перегрузка методов полезна, когда требуется решать подобные задачи с разным набором аргументов. Типичный пример - это нахождение площади треугольника. Площадь можно вычислить по трем сторонам, по двум углам и стороне, по двум сторонам и углу между ними и при многих других наборах аргументов. Считается удобным во всех случаях иметь для метода одно имя, например, Square, и всегда, когда нужно вычислить площадь, не задумываясь вызывать метод Square, передавая ему известные в данный момент аргументы.
Пример этот, может быть, не совсем удачен, поскольку при перегрузке сигнатуры реализаций должны отличаться, а для вычисления площади обычно требуются три аргумента, вообще говоря, одного типа. Так что в этом случае придется использовать искусственные приемы, например, объявляя стороны треугольника типа float, а углы типа - double. Другая возможность - иметь набор методов с разными именами, но с одинаковой сигнатурой.
Перегрузка характерна и для знаков операций. В зависимости от типов аргументов один и тот же знак может выполнять фактически разные операции. Классическим примером является знак операции сложения +, который играет роль операции сложения не только для арифметических данных разных типов, но и выполняет конкатенацию строк.
Перегрузка требует уточнения семантики вызова метода. Когда встречается вызов не перегруженного метода, то имя метода в вызове однозначно определяет, тело какого метода должно выполняться в точке вызова. Когда же метод перегружен, то знания имени недостаточно - оно не уникально. Уникальной характеристикой перегруженных методов является их сигнатура. Перегруженные методы, имея одинаковое имя, должны отличаться либо числом аргументов, либо их типами, либо ключевыми словами (заметьте, с точки зрения сигнатуры ключевые слова ref и out не отличаются). Уникальность сигнатуры позволяет вызвать требуемый перегруженный метод.
Выше уже были приведены четыре перегруженных метода с именем Cube, отличающиеся сигнатурой. Методы отличаются типами аргументов и ключевым словом params. Когда вызывается метод Cube с двумя аргументами, в зависимости от типа будет вызываться реализация, не содержащая аргумент с модификатором params. Когда же число аргументов больше двух, работает реализация, позволяющая справиться с заранее не фиксированным числом аргументов. Заметьте, эта реализация может прекрасно работать и для случая двух аргументов, но полезно иметь частные случаи для фиксированного набора аргументов. При поиске подходящего перегруженного метода частные случаи получают предпочтение в сравнении с общим случаем.
Насколько полезна перегрузка методов? Здесь нет экономии кода, поскольку каждую реализацию нужно задавать явно; нет выигрыша по времени, скорее требуются определенные затраты на поиск подходящей реализации, который может приводить к конфликтам, к счастью, обнаруживаемым на этапе компиляции. В нашем примере вполне разумно было бы отказаться от перегрузки и иметь четыре метода с разными именами, осознанно вызывая метод, применимый к конкретным данным.
Есть ситуации, где перегрузка полезна, недаром она широко используется при построении библиотеки FCL. Возьмем, например, класс Convert, у которого 16 методов с разными именами, зависящими от целевого типа преобразования. Каждый из этих 16 методов перегружен и, в свою очередь, имеет примерно 16 реализаций в зависимости от типа источника. Согласитесь, что неразумно было бы иметь в классе Convert 256 методов вместо 16 перегруженных методов. Впрочем, так же неразумно было бы иметь один перегруженный метод, имеющий 256 реализаций. Перегрузка - это инструмент, которым следует пользоваться с осторожностью и обоснованно.
В заключение этой темы посмотрим, как проводилось тестирование работы с перегруженными методами:
/// <summary> /// Тестирование перегруженных методов Cube() /// </summary> public void TestLoadMethods() { long u = 0; double v = 0; Cube(out u, 7); Cube(out v, 7.5); Console.WriteLine("u= {0}, v= {1}", u, v); Cube(out v, 7); Console.WriteLine("v = {0}", v); Cube(out u, 7, 11, 13); Cube(out v, 7.5, Math.Sin(11.5) + Math.Cos(13.5), 15.5); Console.WriteLine("u= {0}, v= {1}", u, v); }//TestLoadMethods
На рис. 5.3 показаны результаты этого тестирования.
Архитектура проекта
Как обычно, для поддержки примеров этой главы создано Решение с именем Ch5, содержащее консольный проект ProcAndFun. Помимо автоматически созданного класса Program, в проект добавлены три класса - Testing, Account, Account1. В Main процедуре класса Program создается объект testObject класса Testing, вызывающий методы этого класса. Каждый из методов представляет собой тест, позволяющий на примере пояснить излагаемый материал.