165

Допустим есть это:

private int a { get; set; }

Какой в этом смысл, если я могу сделать так:

public int a;
αλεχολυτ
  • 28,987
  • 13
  • 60
  • 119
alex91
  • 3,349

6 Answers6

404

Смотрите, какие есть преимущества у свойства перед полем.

Если ваше свойство определено так:

public int A { get; set; }

— то непосредственных выгод, конечно, нету. Но выгоды придут позже.

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

    private int a;
    private int readcount_a = 0;
    public int A
    {
        get { readcount_a++; return a; }
        set { a = value; }
    }
    

    Вы можете сделать триггер на изменение поля:

    class Data : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
    private int a;
    public int A
    {
        get { return a; }
        set
        {
            if (a == value)
                 return;
            a = value;
            RaisePropertyChanged();
        }
    }
    
    private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    

    }

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

    public int A
    {
        get { return a; }
        set
        {
            Trace.TraceInformation("Changing value of a from {0} to {1}", a, value);
            a = value;
        }
    }
    

    Вы можете навесить проверку значения на корректность при записи, или ленивую инициализацию при чтении.

    class Data
    {
        private string a = null;
        public string A
        {
            get
            {
                return a ?? (a = LazyComputeInitialA());
            }
            set
            {
                if (value == null)
                    throw new ArgumentException(nameof(A) + " cannot be null", nameof(A));
                a = value;
            }
        }
    }
    

    В конце-концов, вы можете не расходовать память на значение, если в большинстве случаев оно одинаковое (как это сделано у DependencyProperty):

    class Data
    {
        static Dictionary<Data, int> aValues = new Dictionary<Data, int>();
    
    public int A
    {
        get
        {
            int result;
            if (aValues.TryGetValue(this, out result))
                return result;
            else
                return -1; // default value
        }
        set
        {
             aValues[this] = value;
        }
    }
    

    }

    [Если вы захотите воспользоваться этим кодом в своём проекте, применяйте улучшенный вариант:

    static readonly ConditionalWeakTable<Data, StrongBox<int>> aValues =
                                      new ConditionalWeakTable<Data, StrongBox<int>>();
    

    public int A { get => aValues.TryGetValue(this, out var tmp) ? tmp.Value : -1; // default value set => aValues.GetOrCreateValue(this).Value = value; }

    В этом варианте исправлены недостатки варианта с Dictionary: в словаре более не хранится ссылка на объект Data (она препятствует удалению объекта сборщиком мусора), а также одновременный доступ из разных потоков не приведёт к проблемам с Dictionary, т. к. ConditionalWeakTable потокобезопасен.]

  2. Вы хотите сделать разную степень видимости у геттера и сеттера, так что например только ваш класс и его наследники смогут установить значение, а считать смогут все. (Это, пожалуй, самое лучшее применение свойств, на мой вкус.)

    public int A { get; protected set; }
    
  3. Вы можете вообще не определять setter, и возвращать какую-то высчитанную вами величину:

    public int A
    {
        get { return b + c; }
    }
    

    (впрочем, такое можно сделать и при наличии сеттера). Например, вы можете предоставлять данные в разных форматах:

    public double Radians { get; set; }
    public double Degrees
    {
        get { return Radians * 180.0 / Math.PI; }
        set { Radians = value * Math.PI / 180.0; }
    }
    
  4. Вы можете установить брейкпойнт на запись или чтение свойства! Брейкпойнты на запись или чтение данных в отладчике управляемого кода Visual Studio пока (по крайней мере до текущей на данный момент версии Visual Studio 2017, версия 15.7) не поддерживаются. Обновление: начиная с .NET Core 3.0, брейкпойнты на изменение данных поддерживаются!

  5. Вы можете задать наличие свойства в интерфейсе, в отличие от поля:

    interface ISupportsA
    {
        int A { get; }
    }
    

    class Data : ISupportsA { public int A { get; set; } }

    Если вы применяете реализацию методов в интерфейсах, появившуюся в C# 8, вы сможете реализовать свойство, используя технику с ConditionalWeakTable, описанную выше. Также при помощи этой техники вы сможете добавить в класс данные в методах расширения.

  6. Вы можете объявить свойство виртуальным! То есть вы сможете переопределить поведение свойства в классах-наследниках. Попробуйте-ка сделать такое с полем.

  7. Свойства не хуже полей в том смысле, что вы можете заставить свойство работать так, будет это просто поле (public int A { get; set; }), но вы не сможете поле заставить работать как свойство. То есть практически всегда лучше «наружу» выставлять свойство, а не поле.

  8. XML-сериализация и WPF-овский Binding работает лишь со свойствами, но не с полями. Да, это можно считать ошибкой во фреймворке, но фактически это так.


Но не излишни ли свойства в языке? Кажется, что вместо свойства можно определить просто две функции:

class Data
{
    private int a;
    public int GetA() { return a; }
    public int SetA(int a) { this.a = a; }
}

Ответ на это таков.

Во-первых, одно свойство вместо двух функций представляет собой логическую группу. В хорошем языке вы говорите то, что думаете. На самом деле вы предоставляете пользователю «переменную» A с дополнительной, часто невидимой снаружи семантикой. Значит, и выглядеть она должна как одна переменная, чтобы пользователи класса думали в тех же терминах, что и вы.

Во-вторых, это читаемость текста. Сравните код со свойствами:

player.car.speed++;

и без них:

getPlayer().getCar().setSpeed(getPlayer().getCar().getSpeed()+1);

Что легче воспринимается?


Справедливости ради, нужно отметить и недостатки свойств по сравнению с полями.

  1. Свойства нельзя использовать как out/ref-параметр, поля можно.

  2. Доступ к полям очень быстр, а вот доступ к свойствам может быть медленным, если код внутри геттера/сеттера медленный. Однако, медленный сеттер или (ещё хуже) геттер считаются порочной практикой, их рекомендуется избегать, чтобы не разрушать ментальную модель «переменная с небольшим довеском».

  3. Доступ к свойству может выбросить исключение или зависнуть, в то время как более простые поля ведут себя крайне просто. Конечно, правильно написанное свойство не будет зависать, а исключения я бы порекомендовал выбрасывать только в случаях, когда пользователь класса нарушил контракт на доступ к полю.


Ещё одно тонкое отличие свойства от поля состоит в том, что геттер возвращает вам копию значения, в то время как при работе с полем вы получаете доступ непосредственно к переменной. При работе со полями reference-типов (то есть, тип которых есть класс) практической разницы нет, так как работа с объектом по копии ссылки не отличается от работы по оригиналу ссылки. Разница, однако, есть, когда поле является изменяемой структурой (хотя, сами по себе изменяемые структуры — плохая идея). Пример случая, когда это важно, во фрагменте кода ниже.


Часто считают, что можно для начала объявить данные как поле, а потом, при необходимости, «превратить» его в свойство. Это лишь отчасти верно: при этом вы теряете бинарную совместимость. Код, который использовал ваш класс, должен быть перекомпилирован, так как на уровне скомпилированного кода обращение к полю и к свойству — не одно и то же. Кроме того, смысл кода может поменяться, приводя к тонким ошибками. Пример из статьи по ссылке выше:

using System;

struct MutableStruct { public int Value { get; set; } public void SetValue(int newValue) { Value = newValue; } }

class MutableStructHolder { public MutableStruct Field; public MutableStruct Property { get; set; } }

class Test {
static void Main(string[] args) { MutableStructHolder holder = new MutableStructHolder(); // Меняет значение holder.Field holder.Field.SetValue(10); // Получает копию holder.Property и изменяет её holder.Property.SetValue(10);

    Console.WriteLine(holder.Field.Value);    // 10
    Console.WriteLine(holder.Property.Value); // 0
}

}


Кстати, согласно Википедии, геттер и сеттер правильно называть акцессор и мутатор соответственно. Вы об этом знали? [Хотя, MSDN пишет просто «методы доступа».]

VladD
  • 206,799
  • "непосредственных выгод, конечно, нету..." - это не совсем верно. Вспомните хотя бы XAML-байндинги в WPF/Silverlight/и т.д., которые полей не признают, зато со свойствами работают. – Mints97 May 03 '15 at 09:29
  • 2
    @Mints97: Это пункт 8 ответа :-) – VladD May 03 '15 at 10:49
  • @VladD: простите, как-то проглядел =) – Mints97 May 03 '15 at 10:50
  • А согласно msdn, оба метода называются акцессорами. Или, по-русски, методами доступа. Кстати, в Википедии нет ссылки на источник... – Pavel Mayorov Apr 27 '16 at 13:59
  • @PavelMayorov: В английской сказано то же самое (хотя, конечно, это не означает автоматически, что в русской принято то же самое). В любом случаем все говорят просто «сеттер», на моей памяти. На MSDN в «ручном» переводе вместо этого пишут «_метод доступа_». – VladD Apr 27 '16 at 15:39
  • Я бы добавил, что у компонентов в дизайнере видны свойства, а поля нет. – 4per Jul 15 '16 at 04:03
  • 1
    @VladD Ещё одно тонкое отличие свойства от поля состоит в том, что геттер возвращает вам копию значения, в то время как при работе со свойством вы получаете доступ непосредственно к переменной. опечатка. – sp7 Aug 22 '17 at 05:25
  • @sp7: Точно! Спасибо, исправил. – VladD Aug 22 '17 at 12:01
  • @VladD, "Класс XmlSerializer способен сериализировать типы, не имеющие ни одного атрибута. По умолчанию он сериализирует все открытые поля и свойства типа." – Андрей NOP Aug 22 '17 at 12:35
  • @Андрей: Хм, перепроверю дома. Вроде ж не умел? – VladD Aug 22 '17 at 16:25
  • @VladD, да не, всегда умел, даже в MSDN написано: https://msdn.microsoft.com/ru-ru/library/bdxxw552(v=vs.110).aspx – Андрей NOP Aug 22 '17 at 16:51
  • @Андрей: И правда! Спасибо, исправил. – VladD Aug 22 '17 at 19:20
  • Решили ещё баллов поднять на супер-ответе? :) –  Aug 22 '17 at 19:25
  • @Aid: Исправил баг :) Все ходы записаны же, и даже комментарий с багрепортом был сегодня. – VladD Aug 22 '17 at 19:37
  • Одно другому не мешает. –  Aug 22 '17 at 19:51
  • "Брейкпойнты на запись или чтение данных в отладчике .NET пока не поддерживаются." Было бы неплохо тут уточнить для какой версии отладчика они не поддерживаются. –  Jul 25 '18 at 08:56
  • @Anamnian: Насколько я понимаю, это так для всех версий вплоть до текущей. Но да, вы правы, дополню ответ. – VladD Jul 31 '18 at 12:58
  • Можно также отметить инкапсуляцию – Viewed Feb 23 '20 at 08:02
  • @Viewed: Вы правы, по сути многие из перечисленных примеров (пункты 1 и 3) и являются примером инкапсуляции. – VladD Feb 24 '20 at 16:46
  • сообщение с самым большим рейтингом на всем ruSo! – tohatsu Jul 08 '20 at 12:23
  • Слышь, мужик, негр у тебя родился - мутатор мыть надо! – rotabor Nov 16 '23 at 19:12
39

Я думаю, что use-case'ов, в которых полезно делать именно поле, довольно таки много. Попробую привести вам один пример, поясняющий мотивацию использования свойства вместо поля.

Представим, что вы разрабатываете библиотеку классов. И реализовали класс как-то так:

class SuperClass
{
    public int a; // То есть, решили, что поля достаточно.
}

Вашей библиотекой пользуются другие разработчики (то есть, клиенты по отношению к вашему коду). И вот оказывается, что вы хотите добавить некую логику чтения или записи a (например, считывание значения из конфига). Полем здесь не обойдешься - делаем свойство:

class SuperClass
{
    public int a { get { /*какая-то логика*/ } set { /*какая-то логика*/ } }
}

Вы поставили библиотеку клиентам и оп-па... Их код больше не работает с вашим. Они не могут просто поставить обновление в виде вашей библиотеки, им придется пересобрать свои приложения, потому что поля a больше нет. Есть свойство, но не поле - внешний интерфейс класса изменился.

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

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

eigenein
  • 3,807
16

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

  • 1
    А чо собственно заминусовали? – vkovalchuk88 May 29 '16 at 12:33
  • @vkovalchuk88 а какое отношение имеет инкапсуляция к написанию { get; set; }? – Pavel Mayorov Aug 16 '16 at 08:00
  • 1
    @PavelMayorov, прямое – vkovalchuk88 Aug 16 '16 at 08:28
  • @PavelMayorov, если углубится в ОПП, то согласно принципу инкапсуляции поля классов всегда должны быть только приватными. А вот для доступа к ним используются функции получения и установки значения. Свойства С#, это своего рода синтаксический сахар, что бы не прописывать функции аксессоры(accessors functions) каждый раз, за Вас это сделает компилятор. Вот если бы вы писали на С++/Java по всем правилам, то на каждое свойство создавали бы свои две функции установки/получения значения и никаких public полей. – Alex Krass Aug 16 '16 at 09:31
  • 6
    @AlexKrass чаще всего такую вещь как { get; set; } можно увидеть в DTO, а DTO не является классом в понимании ООП. Кроме того, есть мнение, что конструкция { get; set; } нарушает инкапсуляцию столь же качественно, как и открытое свойство. – Pavel Mayorov Aug 16 '16 at 10:21
6

Плюсов очень много.
Но важен тот, который приносит пользу в нужный момент.
Эта вещь мне принесла пользу.
Был класс, ну допустим с A. Этот класс использовали из кучи мест. Даже не знаешь из каких. А обновить надо сейчас, через 15 минут. Поиск использования показывается 1100 позиций, а надо поменять только там, где в него что-то пишем.

Делаем из public int A; ->

public int A {get; private set;}

И любое присвоение выпадет списком ошибок.

Legionary
  • 1,480
  • 1
    И как это решит проблему,если бы стояло авто-свойство до этого? Чтобы поменялось в этих 1100 позиций использования? – CSharpUser Oct 23 '20 at 02:18
4

смысл есть, если связываешь скрипты к примеру или хочешь видеть значение в инспекторе, если приват-не видно паблик-видно

One Nox
  • 89
4

В дополнение к подробнейшему ответу @VladD Еще удобно инициализировать:

class Filter {
     public int ID { get; set;}
     public string Name { get; set;}
}

Filter filter = new Filter { ID = 15, Name = "Vasya" };
4per
  • 2,696
  • 2
    С полями такого нельзя повернуть? Ответ – можно: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers читаем первое предложение – Андрей NOP Nov 29 '18 at 12:19