Сперва я подумал, что стоит начать статью с описания основного назначения свойств в языке C#, но потом понял, что с этим можно на самом деле “развернуться” на целую статью. Поэтому, чтобы не затягивать со вступительной частью, я начну сразу с конкретной задачи.
Как известно, в подавляющем большинстве случаев свойства применяются, чтобы скрыть private или protected поле класса. То есть свойства в данном случае помогают реализовать инкапсуляцию данных и методов работы с ними. Рассмотрим простой пример.
В данном примере определён класс Car, который накладывает ограничение на поле maxSpeed с тем, чтобы оно имело значение лишь в определённом диапазоне. У этого кода есть довольно существенная проблема. К сожалению, свойство MaxSpeed защищает поле maxSpeed только от присвоения некорректной скорости извне класса Car. Другие методы и свойства класса Car могут осуществить присвоение произвольного значения полю maxSpeed. Иногда это нормально, а иногда это опасно. Посмотрите на следующий (опасный) фабричный метод в классе Car:
Очевидно, этот метод может создать машину с максимальной скоростью большей, чем значение константы MAX_KNOWN_CAR_SPEED. Таким образом, задача состоит в том, чтобы переменную maxSpeed скрыть как от кода внешнего по отношению к Car, так и от самого класса Car. Пусть даже сам класс осуществляет доступ к этому полю, используя метод. А уже метод обеспечит гарантии корректности присваимового значения. Печально, но заставить всех участников проекта использовать свойство MaxSpeed вместо поля maxSpeed невозможно. Да и сам я замечал, что забываю о такого рода проблемных полях.
Люди, практикующие тру-ООП программирование скажут, что задача не стоила рассмотрения, потому что решение её простое — инкапсулировать maxSpeed в отдельный класс Speed и всего делов. Однако, на мой взгляд, в части случаев это напоминает стрельбу из пушки по воробьям и заводить отдельный класс на каждое подобное свойство это стиль на любителя. Хотя, я согласен, что идеологически такой подход наиболее чистый.
Мы же с коллегой нашли другое решение. У решения есть свои недостатки и некоторые ограничения. Итак, ниже представлен класс, который позволит легко описывать свойства, инкапсулирующие такие “проблемные” поля:
А вот и пример описания проблемного поля и свойства:
Работать с новым полем придётся, используя методы Get() и Set():
var currentSpeed = mazda.NewMaxSpeed.Get()
mazda.NewMaxSpeed.Set(currentSpeed + 10d);
Да, коллеги. Это, конечно, не супер сексуальный код. Пример работы с полем напоминает мне програмиирование на Java. Однако, основная задача решена, причём относительно малой кровью. Теперь при обращению к NewMaxSpeed как извне Car, так и внутри него, будет осуществляться одна и та же проверка на вхождения нового значения в заданный диапазон.
Признаюсь, мне бы хотелось видеть возможность полного сокрытия поля свойством в самом языке. Мой коллега видит себе примерно такой синтаксис для этого дела:
Тот же вариант, что был приведён мной, заставляет работать с полем через противные методы Get, Set, не допуская прямых присвоений. Преодолеть этого мне так и не удалось. Если описать оператор неявного приведения, позволящий писать double x = car.MaxSpeed; довольно просто, то вот реализовать возможность использования car.MaxSpeed = 10d; оказалось невозможно.
Ещё одна гадость, связанная с предложенным решением заключается в невозможности указания разных модификаторов доступа для геттера и сеттера, что, разумеется, является большим минусом.
Вопрос сериализации таких полей/свойств я даже не рассматривал. Полагаю, тут могут возникнуть сложности. Хотя не исключаю, что простого “навешивание” атрибута Serializable на класс HidingProperty может оказаться достаточно.
Пока мне трудно сказать как часто в пределах нашего проекта мы с коллегой будем обращаться к этому решению. Однако я не очень привередлив к стилю кода и в домашних работах класс применять буду.
Спасибо за внимание! Буду признателен за критику решения и новые мысли.
Постановка задачи
Как известно, в подавляющем большинстве случаев свойства применяются, чтобы скрыть private или protected поле класса. То есть свойства в данном случае помогают реализовать инкапсуляцию данных и методов работы с ними. Рассмотрим простой пример.
- class Car
- {
- const double MINIMAL_SPEED = 0d;
-
- const double MAX_KNOWN_CAR_SPEED = 1229.78d;
-
- private double maxSpeed;
-
- public double MaxSpeed
- {
- get { return maxSpeed; }
- set
- {
- if (value < MINIMAL_SPEED || value > MAX_KNOWN_CAR_SPEED)
- throw new ArgumentOutOfRangeException("MaxSpeed");
- maxSpeed = value;
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
В данном примере определён класс Car, который накладывает ограничение на поле maxSpeed с тем, чтобы оно имело значение лишь в определённом диапазоне. У этого кода есть довольно существенная проблема. К сожалению, свойство MaxSpeed защищает поле maxSpeed только от присвоения некорректной скорости извне класса Car. Другие методы и свойства класса Car могут осуществить присвоение произвольного значения полю maxSpeed. Иногда это нормально, а иногда это опасно. Посмотрите на следующий (опасный) фабричный метод в классе Car:
- class Car
- {
- // ...
- public static Car CreateRandomCar()
- {
- return new Car()
- {
- maxSpeed = (new Random()).NextDouble() * double.MaxValue,
- };
- }
- }
* This source code was highlighted with Source Code Highlighter.
Очевидно, этот метод может создать машину с максимальной скоростью большей, чем значение константы MAX_KNOWN_CAR_SPEED. Таким образом, задача состоит в том, чтобы переменную maxSpeed скрыть как от кода внешнего по отношению к Car, так и от самого класса Car. Пусть даже сам класс осуществляет доступ к этому полю, используя метод. А уже метод обеспечит гарантии корректности присваимового значения. Печально, но заставить всех участников проекта использовать свойство MaxSpeed вместо поля maxSpeed невозможно. Да и сам я замечал, что забываю о такого рода проблемных полях.
Решение
Люди, практикующие тру-ООП программирование скажут, что задача не стоила рассмотрения, потому что решение её простое — инкапсулировать maxSpeed в отдельный класс Speed и всего делов. Однако, на мой взгляд, в части случаев это напоминает стрельбу из пушки по воробьям и заводить отдельный класс на каждое подобное свойство это стиль на любителя. Хотя, я согласен, что идеологически такой подход наиболее чистый.
Мы же с коллегой нашли другое решение. У решения есть свои недостатки и некоторые ограничения. Итак, ниже представлен класс, который позволит легко описывать свойства, инкапсулирующие такие “проблемные” поля:
- public class HidingProperty<T>
- {
- public delegate T1 Getter<T1>(ref T1 currentValue);
- public delegate void Setter<T2>(ref T2 currentValue, T2 newValue);
-
- private T _storedValue;
- private Getter<T> _getter;
- private Setter<T> _setter;
-
- public HidingProperty(Getter<T> getter, Setter<T> setter)
- : this(default(T), getter, setter) { }
-
- public HidingProperty(T initialValue, Getter<T> getter, Setter<T> setter)
- {
- _storedValue = initialValue;
- _getter = getter;
- _setter = setter;
- }
-
- public void Set(T newValue)
- {
- _setter(ref _storedValue, newValue);
- }
- public T Get()
- {
- return _getter(ref _storedValue);
- }
- }
* This source code was highlighted with Source Code Highlighter.
А вот и пример описания проблемного поля и свойства:
- private HidingProperty<double> NewMaxSpeed = new HidingProperty<double>(
- (ref double currentValue) => { return currentValue; },
- (ref double currentValue, double newValue) =>
- {
- if (newValue < MINIMAL_SPEED || newValue > MAX_KNOWN_CAR_SPEED)
- throw new ArgumentOutOfRangeException("NewMaxSpeed");
- currentValue = newValue;
- }
- );
* This source code was highlighted with Source Code Highlighter.
Работать с новым полем придётся, используя методы Get() и Set():
var currentSpeed = mazda.NewMaxSpeed.Get()
mazda.NewMaxSpeed.Set(currentSpeed + 10d);
Да, коллеги. Это, конечно, не супер сексуальный код. Пример работы с полем напоминает мне програмиирование на Java. Однако, основная задача решена, причём относительно малой кровью. Теперь при обращению к NewMaxSpeed как извне Car, так и внутри него, будет осуществляться одна и та же проверка на вхождения нового значения в заданный диапазон.
Недостатки, ограничения, побочные эффекты
Признаюсь, мне бы хотелось видеть возможность полного сокрытия поля свойством в самом языке. Мой коллега видит себе примерно такой синтаксис для этого дела:
- public double Speed
- {
- double speed;
- get { // Код геттера }
- set { // Код сеттера }
- }
* This source code was highlighted with Source Code Highlighter.
Тот же вариант, что был приведён мной, заставляет работать с полем через противные методы Get, Set, не допуская прямых присвоений. Преодолеть этого мне так и не удалось. Если описать оператор неявного приведения, позволящий писать double x = car.MaxSpeed; довольно просто, то вот реализовать возможность использования car.MaxSpeed = 10d; оказалось невозможно.
Ещё одна гадость, связанная с предложенным решением заключается в невозможности указания разных модификаторов доступа для геттера и сеттера, что, разумеется, является большим минусом.
Вопрос сериализации таких полей/свойств я даже не рассматривал. Полагаю, тут могут возникнуть сложности. Хотя не исключаю, что простого “навешивание” атрибута Serializable на класс HidingProperty может оказаться достаточно.
Выводы
Пока мне трудно сказать как часто в пределах нашего проекта мы с коллегой будем обращаться к этому решению. Однако я не очень привередлив к стилю кода и в домашних работах класс применять буду.
Спасибо за внимание! Буду признателен за критику решения и новые мысли.