Няколко истории за разликите между Release и Debug
Всички разработчици знаят, че изпълнението на версията за освобождаване може да се различава от версията за отстраняване на грешки. В тази статия ще ви разкажа няколко случая от реалния живот, когато такива разлики са довели до погрешно изпълнение на програмата. Примерите не са много сложни, но могат да ви предпазят от настъпване на гребла.
Всъщност всичко започна с факта, че се появи грешка, че приложението се срива по време на някои операции. Случва се често. Грешката не искаше да бъде възпроизведена във версията за отстраняване на грешки. Това понякога се случва. Тъй като някои от библиотеките в приложението бяха написани на C ++, първата мисъл беше нещо като „някъде са забравили да инициализират променливата или нещо подобно“. Но всъщност същността на грешката беше в управлявания код, въпреки че не би могло да се направи и без неуправляван код.
И кодът се оказа нещо подобно:
public Wrapper() това .Obj = CreateUnmObject(); >
Wrapper() това .Dispose( false); >
защитен виртуален void Dispose( bool disposing) if (disposing) >
this .ReleaseUnmObject( this .Obj); този .Obj = IntPtr .Zero; >
public void Dispose() това .Dispose( true); GC.SuppressFinalize( това); > >
* Този изходен код беше подчертан с инструмента за открояване на изходния код.
По принцип, почти каноничното изпълнение на модела IDisposable („практически“ - тъй като няма разположена променлива, вместо това нулира указателя), е напълно стандартен неуправляем клас обвивка на ресурси.
Класът е използван нещо подобно:
* Този изходен код беше подчертан с инструмента за открояване на изходния код.
Естествено, внимателният читател веднага ще забележи, че wr обектът трябва да се нарича Dispose, тоест да обвие всичко с използваща конструкция. Но на пръв поглед причинататова не би трябвало да повлияе на падането, тъй като разликата ще бъде в това дали ресурсът е детерминистично изчистен или не.
Но всъщност има разлика в монтажа на освобождаването. Факт е, че обектът wr става достъпен за събирача на отпадъци веднага след стартирането на метода DoCalculations, тъй като вече няма нито един „жив“ обект, който да се позовава на него. Това означава, че wr може (и така се случи) да бъде унищожен по време на изпълнението на DoCalculations и указателят, предаден на този метод, става невалиден.
Опаковането на извикването на DoCalculations в използване (Wrapper wr = new Wrapper()) ще реши проблема, тъй като извикването на Dispose в блока finally ще попречи на алчния събирач на боклук да „изяде“ обекта преди време. Ако по някаква причина не можем или не искаме да извикаме Dispose (например WPF изобщо не харесва този шаблон), тогава ще трябва да вмъкнем GC.KeepAlive (wr) след извикване на DoCalculations.
Истинският код, разбира се, беше по-сложен и не беше толкова лесно да се види грешката в него, както в примера.
Защо грешката се появи само във версията Release и след това не беше стартирана от студиото (ако прикачите дебъгер по време на изпълнение, грешката ще се повтори)? Тъй като в противен случай котвите се добавят към всички локални референтни променливи, така че да оцелеят до края на текущия метод, това се прави изрично в името на удобството при отстраняване на грешки.
Имало едно време проект, в който за достъп до ресурси се използвал мениджър, който, използвайки ключ от низ, получавал ресурси от различни типове от даден сборник. За да се улесни писането на кода, беше написан следният метод:
public string GetResource( string key) Assembly assembly = Assembly .GetCallingAssembly(); върни това .GetResource(assembly, key); >
* Този изходен код беше маркиран сМаркиране на изходния код.
След мигриране към .Net 4 някои ресурси внезапно спряха да бъдат локализирани. И въпросът тук отново е да се оптимизира версията на изданието. Факт е, че във версия 4 на dotnet компилаторът може да вгражда извиквания към кода на методите на други сборки.
За да "усетите разликата" предлагаме следния пример:
dll1: публичен клас Class1 public void Method1() Конзола .WriteLine( new StackTrace()); > >
dll2: публичен клас Class2 публичен void Method21() този .Method22(); >
public void Method22() ( нов Class1()).Method1(); > >
dll3: class Program static void Main( string [] args) ( new Class3()).Method3(); > > клас Class3 public void Method3() ( нов Class2()).Method21(); > >
* Този изходен код беше подчертан с инструмента за открояване на изходния код.
Ако компилирате в конфигурация за отстраняване на грешки (или ако стартирате процеса от под студиото), тогава получаваме честен стек за извикване: в ClassLibrary1.Class1.Method1() в ClassLibrary2.Class2.Method22() в ClassLibrary2.Class2.Method21() в ConsoleApplication1.Class3.Method3( ) в ConsoleApplication1.Program.Main(String []args)
Ако е компилиран под .Net версия до 3.5 включително в изданието: в ClassLibrary1.Class1.Method1() в ClassLibrary2.Class2.Method21() в ConsoleApplication1.Program.Main(String[] args)
И под .Net 4 в конфигурацията на изданието ще получим: в ConsoleApplication1.Program.Main(String[] args)
Моралът тук е прост - не свързвайте логиката със стека на повикванията, както и да се изненадате от необичайния стек в изключенията в дневника на версията на изданието. По-специално, ако се опитвате да намерите причината за изключение въз основа единствено на неговия стек за извиквания, тогава трябваимайте предвид, че ако стекът завършва на метода Method1, тогава в кода той (изключението) може да бъде генериран в един от малките методи, които се извикват в тялото на Method1.
Също така, за всеки случай си струва да запомните, че можете да попречите на компилатора да вгради метод, като го маркирате с атрибута [MethodImpl(MethodImplOptions.NoInlining)], нещо като аналог на __declspec(noinline) във VC++.
Вместо заключение
Светът на бъгове, които се появяват само в изданието, е наистина неограничен и не си поставих за цел да правя пълен преглед на него. Просто исках да споделя собствения си опит или по-скоро по-интересната част от него. Е, остава само да пожелаем на читателите по-малко да срещат такива грешки в работата си.
Hardcore conf в C++. Каним само професионалисти.