Защо типовете числа в C# са неизменни?

Защо ints и doubles са неизменни? Каква е целта на връщането на нов обект всеки път, когато искате да промените стойността?

Причината да попитам е, че правя клас: BoundedInt, който има стойност и горна и долна граница. Така че се чудех: трябва ли да направя и този тип неизменен? (Или трябва да е struct?)


person Nobody    schedule 20.10.2010    source източник
comment
Отваряте огромни кутии с червеи с тези въпроси. Моята препоръка е да помислите два пъти дали да се опитате да поставите валидирането на вашите данни в самите данни. Валидирането има повече смисъл на по-високо ниво на абстракция, отколкото базовите типове данни. Например, може да се наложи да имате една част от диапазон от данни, обвързана със стойността на друга част от данни.   -  person Merlyn Morgan-Graham    schedule 21.10.2010
comment
@Merlyn: Какво ще кажете за данни с неявни граници, като дължина [0, ∞), ъгъл [0, 360) или резултат от хвърляне на зарове [1,6]? Със сигурност не искам да съм висока -180 см.   -  person Seth    schedule 21.10.2010
comment
@Seth: Това са специални случаи, които не определят нашата архитектура. Дали стойност 361 или -1 е невалидна зависи изцяло от това, като се интерпретират, а не от стойностите сами по себе си.   -  person Steven Sudit    schedule 21.10.2010
comment
@Seth: Ограничение по подразбиране, свързано с тип, може да бъде полезно (напр. 6-странна матрица), но много често ще искате да персонализирате това ограничение. Единиците са свързана грижа. По-добър подход е да се използва система за решаване/картографиране на ограничения или рамка за програмиране, базирана на договор (напр. spec#).   -  person Merlyn Morgan-Graham    schedule 21.10.2010
comment
трябва да слушате дискусията на чичо Боб в DotNetRocks за „бъдещето на ООП“   -  person lawphotog    schedule 15.04.2016


Отговори (6)


Първо:

Каква е целта на връщането на нов обект всеки път, когато искате да промените стойността?

Мисля, че може да грешите за това как работят типовете стойности. Това не е някаква скъпа операция, както може би си представяте; това е просто презаписване на данни (за разлика от, например, динамично разпределение на нова памет).

Второ: ето един много прост пример защо числата са неизменни:

5.Increase(1);
Console.WriteLine(5); // What should happen here?

Разбира се, това е измислен пример. Така че нека разгледаме още няколко замесени идеи.

Променлив референтен тип

Първо, има следното: ами ако Integer беше променлив референтен тип?

class Integer
{
    public int Value;
}

Тогава бихме могли да имаме код като този:

class Something
{
    public Integer Integer { get; set; }
}

И:

Integer x = new Integer { Value = 10 };

Something t1 = new Something();
t1.Integer = x;

Something t2 = new Something();
t2.Integer = t1.Integer;

t1.Integer.Value += 1;

Console.WriteLine(t2.Integer.Value); // Would output 11

Това изглежда противоречи на интуицията: че редът t2.Integer = t1.Integer просто ще копира стойност (всъщност го прави; но тази "стойност" всъщност е препратка) и по този начин t2.Integer ще остане независим от t1.Integer.

Променлив тип стойност

Това може да се подходи по друг начин, разбира се, като се запази Integer като тип стойност, но се запази неговата променливост:

struct Integer
{
    public int Value;

    // just for kicks
    public static implicit operator Integer(int value)
    {
        return new Integer { Value = value };
    }
}

Но сега да кажем, че правим това:

Integer x = 10;

Something t = new Something();
t.Integer = x;

t.Integer.Value += 1; // This actually won't compile; but if it did,
                      // it would be modifying a copy of t.Integer, leaving
                      // the actual value at t.Integer unchanged.

Console.WriteLine(t.Integer.Value); // would still output 10

По принцип неизменността на стойностите е нещо, което е силно интуитивно. Обратното е силно неинтуитивно.

Предполагам, че е субективно, честно казано ;)

person Dan Tao    schedule 20.10.2010
comment
Добри подобрения в последната ви редакция. Ако можех да гласувам за втори път (в така наречения Чикагски стил), щях да го направя. - person Steven Sudit; 21.10.2010
comment
Хрумва ми, че Java има int примитив, който не е обект, както и Integer примитивен обвиващ клас. Това означава, че вашите измислени примери всъщност не са чак толкова измислени. :-) - person Steven Sudit; 21.10.2010
comment
Помислете за реален тип структура като Rectangle. Кое е по-ясно: someRect.Width *= 2; или someRect = someRect.withNewWidth(someRect.Width*2); Лошо е, че .net предоставя такава слаба поддръжка за променливи свойства на типа стойност, но често пъти семантиката на променлив тип стойност е много по-чиста от всичко друго. - person supercat; 21.10.2011
comment
@supercat: Наполовина съм на този. Определено съм съгласен, че променливите типове стойности не са лоши сами по себе си. Всъщност намирам приложения за тях доста често. Неприятният факт обаче е, че те могат да бъдат трудни дори за интелигентни разработчици. Вземете вашия пример: someRect.Width *= 2; наистина изглежда хубаво и ясно; но след това направете това свойство и изведнъж кодът не работи: someObject.Rectangle.Width *= 2; Разбира се, последният няма да се компилира; но за съжаление това би (ако приемем, че методът съществува): someObject.Rectangle.Scale(2); Както при всички неща, има аргументи и от двете страни. - person Dan Tao; 21.10.2011
comment
@Dan Tao: Смятам, че .net има няколко слабости в обработката на типове стойности: (1) няма const ref параметри, нито средство за разграничаване кои struct методи променят това; (2) никаква поддръжка за какъвто и да е тип свойство, различно от достъп за четене, достъп за запис или двойка (достъп за четене/достъп за запис). Мисля, че всички проблеми с изменяемите структури се свеждат до тези две неща. Като се има предвид #1, между другото, бих се съгласил, че с Ерик Липерт тези структури почти никога не трябва да променят това в своите методи. - person supercat; 21.10.2011
comment
@Dan Tao: Ако методът за мащабиране беше статичен Rectangle.Scale(ref theRectangle, double Amount); Не мисля, че нещо би се компилирало, което не би трябвало. Ако методът на разширение може да има този подпис, той ще може да се използва като обикновен метод, когато трябва, но ще откаже да компилира, когато е подходящо. Моето мнение за идеята, че променливите типове стойност са зли, е, че самомутиращите се типове са зли в контекста на .net, като се има предвид, че неща като бокс, които структурата не може да предотврати, не предполагат самомутация, но иначе те не е зло - просто ограничено в някои контексти. - person supercat; 21.10.2011
comment
@Dan Tao: Ако имах моите druthers, щеше да е възможно свойство тип стойност от тип T да бъде метод с поне един отворен общ тип U [V, W...], който ще приеме метод, който има a ref параметри от тип T и U [V, W...] и ще го извика с елемент от тип T заедно с предадения в U [V, W...]. someObject.theRect.x=someInteger ще се преведе в someObject.theRect__invoker‹Integer›(ref someInteger, (ref theRect, ref param1)=›{theRect.x = param1;}. Предаването на типове стойности по референция е много ефективно. Единственият проблем е, че броят на необходимите реф параметри ще варира. - person supercat; 21.10.2011

Целочислените променливи са променливи. Въпреки това, целочислените литерали са константи, следователно неизменни.

int i = 0;

// Mutation coming!
i += 3;

// The following line will not compile.
3 += 7;

Възможно е да направите цяло числово поле неизменно, като използвате readonly. По същия начин, цяло число свойство може да бъде само за получаване.

person Steven Sudit    schedule 20.10.2010
comment
Благодаря ви за гласуването против. Сега ще съм благодарен, ако предложите обяснение, за да мога да се поуча от грешката си. - person Steven Sudit; 21.10.2010
comment
@Robert: Не каза, че е така. :-) - person Steven Sudit; 21.10.2010
comment
Би било чудесно, ако някой посочи грешката. В противен случай може да прекарам остатъка от живота си, въобразявайки си, че тук няма грешка. - person Steven Sudit; 21.10.2010
comment
Като се има предвид мълчанието и гласовете за, подозирам, че първоначалното гласуване против може да е било погрешка. Ако това не е така, аз съм отворен за корекция. - person Steven Sudit; 21.10.2010
comment
Аргументът не е 3 неизменен... той е неизменно цяло число. Огромна разлика. i += 3 е същото като i = i + 3... все още е неизменно. неизменен обект е обект, чието състояние не може да бъде променено след създаването му. Вие не редактирате i НА МЯСТО. Редактирате го и го връщате обратно в нов i. Точно като здравей + свят! означава НИЩО без s = ... от лявата страна. - person WernerCD; 21.10.2010
comment
@WernerCD: Благодаря за отговора. За да бъде нещо неизменно, то трябва да бъде инициализирано до някакво състояние, което никога не може да се промени. Това ясно се отнася за неизменен клас като System.String, тъй като има разлика между екземпляра и неговите препратки. За структура като System.Windows.Point има разлика между замяна на цялото нещо и просто промяна на част, така че предотвратяването на последното е вид неизменност. Въпреки това, System.Int32 е стойност без части, така че не е възможно разграничение между замяна на едро и модификация. - person Steven Sudit; 21.10.2010
comment
Ако декларирате поле int в клас, то е напълно променливо, тъй като може да се променя по желание. Не само можете да използвате семантика за заместване, като _x = 7;, можете също да използвате семантика за модификация, като _x += 7;. От друга страна, ако го направите readonly, нито един вид промяна не е възможен. Сравнете това с, да речем, System.Text.StringBuilder, за което Append е възможно дори когато полето е readonly, но заместването не е. - person Steven Sudit; 21.10.2010
comment
Когато има разлика между екземпляр и неговите препратки или цяло и неговите части, тогава можем да говорим за неизменност. Но разграничението се разпада за типове стойности със собствена стойност, като примитивните числа. Няма начин, дори по принцип, да отговорим на въпроса дали променяме цялото число или го заместваме. Следователно, противно на това, което казахте, целите числа не са неизменни в никакъв смислен смисъл, въпреки че могат да бъдат направени неизменни чрез readonly и const. - person Steven Sudit; 21.10.2010
comment
@StevenSudit Мисля, че Immutable не означава, че дадена променлива може да бъде модифицирана или не. Това означава дали създавате ново копие в паметта или просто променяте стойността на оригиналния адрес. И когато извикате int a=5; a=6, той създава нова стойност 6 в стека, вместо да промени 5 на 6. Вижте ImmutableList забележка, int прави същото. - person joe; 13.12.2018
comment
Знам, че това е вид семантика и може да има различно значение при различни обстоятелства. Но според документ Int32 е неизменен тип стойност, който ...... и Членовете, които изглежда променят състоянието на екземпляра, всъщност връщат нов екземпляр, инициализиран с новата стойност - person joe; 13.12.2018
comment
@joe Това, което казах преди почти десетилетие, все още е в сила: няма принципна основа за разграничение между създаването на ново копие на int и модифицирането му. То няма друга идентичност освен своята стойност, така че промяната на стойността му непременно променя неговата идентичност. - person Steven Sudit; 20.05.2019

Като променлив обект, трябва да заключите променлива int, преди да я промените (във всеки многонишков код, който пише във вашия int от отделни нишки).

Защо? Да приемем, че увеличавате int, като това:

myInt++

Под капака това е 32-битово число. Теоретично, на 32-битов компютър можете да добавите 1 към него и тази операция може да е атомарна; това ще бъде изпълнено в една стъпка, защото ще бъде изпълнено в регистър на процесора. За съжаление, не е; има нещо повече от това.

Какво ще стане, ако друга нишка промени това число, докато е в средата на увеличаване? Вашият номер ще се повреди.

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

Това е основният принцип зад функционалното програмиране; като правите обектите неизменни и връщате нови обекти от функции, вие получавате безопасност на нишката безплатно.

person Robert Harvey    schedule 20.10.2010
comment
Не съм сигурен, че това следва. Всъщност кодът, който показвате, не е безопасен за нишки. Ще трябва да използвате Interlocked.Increment, за да гарантирате това. - person Steven Sudit; 21.10.2010
comment
Трябва също да спомена, че е невъзможно да се заключи int, тъй като не е референтен тип. - person Steven Sudit; 21.10.2010
comment
Благодаря, че променихте отговора си, но съжалявам, че трябва да кажа, че все още не е правилен. Променливата int е променлива, както се вижда от факта, че можете да я увеличавате. И операцията за нарастване, както споменахме, не е атомарна. Въпреки че обикновено се изпълнява като INC код на операция, това не е атомарно с префикс LOCK. Може да е единичен код на операцията, но се превежда като извличане и съхраняване, което означава, че друго ядро ​​може да спечели състезание чрез съхраняване между тези две стъпки. - person Steven Sudit; 21.10.2010
comment
@Steven: Всъщност никога не казвам в отговора си, че int е неизменно, нито казвам, че увеличението е атомарно, а само че може да е атомарно, ако беше направено в една стъпка. Увеличаването вероятно не е най-добрият пример, тъй като така или иначе е многостъпково (връща стойност, след което увеличава). Искам да кажа, че можете да върнете нов обект и да получите безопасност на нишката, при условие че можете да гарантирате, че оригиналният обект няма да мутира по време на вашата операция. Можете да направите тази гаранция, като създадете безопасно за нишки копие на вашия оригинален обект, преди да извършите вашата операция. - person Robert Harvey; 21.10.2010
comment
Разбирам, че използвате многобройни противоположни факти като част от вашето обяснение, но те не изглеждат ясно идентифицирани като неверни. - person Steven Sudit; 21.10.2010
comment
@Steven: Не знам какво означава това. Отговорът казва това, което казва, нито повече, нито по-малко. Мисля, че се опитваш да прочетеш повече в него, отколкото всъщност има. - person Robert Harvey; 21.10.2010
comment
Контрафактът е условно условие, за което е известно, че е невярно. Ако прасетата имаха крила, тогава беконът щеше да е кошер е един пример. Във вашия отговор вие казвате неща като: Ако int бяха променливи, тогава ще трябва да ги заключим, преди да ги променим. Проблемът е, че това не е правилно: int променливите са променливи, докато int константите са (както всички константи) неизменни. Също така трябва да използваме заключване (или еквивалент), за да направим промените атомарни. Това помага ли да се обясни безпокойството ми? - person Steven Sudit; 21.10.2010
comment
Робърт, просто искам да се извиня, ако съм се скрил като недостатъчно ясен или прекалено придирчив. Мотивацията ми е да направя отговора ви възможно най-добър, а не да бъда критичен сам по себе си. - person Steven Sudit; 21.10.2010

Има смисъл да има BoundedInt като променлив тип, защото той представлява променлива, която във всеки един момент има конкретна стойност и тази стойност може да бъде променена, но само в определен диапазон.

Въпреки това целите числа сами по себе си не са променливи, така че не трябва да бъдат променливи.

person Mark Byers    schedule 20.10.2010

Всичко със семантика на стойността трябва да бъде неизменно в C#.

Променливите класове не могат да имат семантика на стойността, защото не можете да замените оператора за присвояване.

MyClass o1=new MyClass();
MyClass o2=o1;
o1.Mutate();
//o2 got mutated too
//=> no value but reference semantics

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

MyStruct S1;
MyStruct S2{get;set;}

S1.Mutate(); //Changes S1
S2.Mutate();//Doesn't change S2

Ето защо не ми харесва, че повечето векторни библиотеки използват мутиращи методи като Normalize в тяхната векторна структура.

person CodesInChaos    schedule 20.10.2010

Работя по академичен проект с невронни мрежи. Тези мрежи правят тежки изчисления с двойници. Пускам го в amazon cloud дни наред на 32 основни сървъра. При профилиране на приложението най-големият проблем с производителността е разпределението на двойно!! Би било честно да има специално пространство от имена с променливи типове. "небезопасни" ключови думи могат да бъдат наложени за допълнителна предпазна мярка.

person random    schedule 17.08.2014