حين يرفع constructor في Delphi استثناء، يستدعي RTL الـ destructor فورًا على النسخة ناقصة البناء، فيجب على كل destructor أن تتعامل مع حقول لم تسند قط. يطبق HotXLS تلك القاعدة على destructors خاصته، بما فيها TXLSXRichText وTXLSXCells: فشل تخصيص داخل تلك الـ constructors يصل إلى كودك كـ EOutOfMemory الأصلي، لا كـ access violation ثانوية ترفع أثناء التنظيف
صنف الخطأ هذا قديم وسهل الفقدان: خدمة تحول مصنف 300 MB على خادم مشغول، تضيق الذاكرة، ويعرض log لـ access violation داخل destructor. لا شيء في تلك الرسالة يقول "نفدت الذاكرة"، ولا شيء يشير إلى أن المحاولة لاحقًا ستنجح، وتشير stack trace إلى destructor يبدو صحيحًا تمامًا. الاستثناء الأصلي عاش بضع ميكروثوان ثم استبدل. وتمشي هذه المقالة في سبب حدوث ذلك، وكيف يبني HotXLS الـ destructors خاصته لتجنبه، وكيف تكتب أصنافك الخاصة فلا تفعله أبدًا
ماذا يحدث حين يرفع constructor في Delphi؟
يدمر الكائن عنك قبل أن يصل الاستثناء إلى المستدعي، ويعمل destructor الخاصك على الحالة التي بلغها constructor أيًا كانت. التسلسل ثابت من المترجم وRTL: يخصص NewInstance النسخة وتملا InitInstance أصفارًا، فيبدأ كل مرجع كائن بـ nil وكل عدد صحيح بـ 0 وكل سلسلة فارغة. ثم يعمل جسم constructor داخل معالج استثناء ضمني. فإن رفع شيء في ذلك الجسم، استدعى الكود المولد الـ destructor الافتراضي: يعمل BeforeDestruction، ثم Destroy الخاصك، ثم يحرر FreeInstance الذاكرة، وأخيرًا يرفع الاستثناء من جديد. لا يعمل AfterConstruction أبدًا، والمتغير على يسار X := TFoo.Create لا يسند أبدًا
فيرى الـ destructor كائنًا توقف عند سطر اعتباطي من constructor خاصه: الحقول المسندة قبل الفشل تحمل كائنات حقيقية، والحقول بعده ما تزال nil. والـ destructor المكتوب بافتراض أن البناء اكتمل سيعمل مرجعًا لأحد تلك الحقول nil ويرفع access violation خاصة به
ذلك الاستثناء الثاني هو الضرر الحقيقي: لا تسلسل Delphi الاستثناءات المرفوعة أثناء هذا التنظيف، فتحل الـ access violation محل EOutOfMemory، ويهمل الاستثناء الأصلي، ولا يرى المستدعي أبدًا سوى الـ AV. والتشخيص المفيد، "فشل تخصيص، حاول مجددًا بذاكرة أكثر"، رحل، ومكانه شيء يشبه فساد الذاكرة
من أين تأتي الـ access violation الثانوية؟
تأتي من destructor يفترض أن constructor خاصه اكتمل ويلمس حقل قائمة يساوي nil حين يفشل أول تخصيص داخلي تمامًا. يبين صنفا HotXLS شكلها بوضوح: كل من TXLSXRichText.Create المستقلة والنسخة المملوكة للتعليق TXLSXRichText.Create(AOwner: TXLSXComment) تستدعيان inherited Create ثم تخصصان FRuns := TList.Create. و تفعل TXLSXCells.Create(AOwnerSheet: TXLSXWorksheet) الشيء نفسه بـ FList := TList.Create قبل أن تبني جداول سلاسلها المعبأة وتخزين الخلايا المعبأ. وفي Lib/lxHandleX.pas يحرس الـ destructorان تلك الحقول:
procedure TXLSXRichText.ClearCore;
var
i: Integer;
begin
if FRuns=nil then Exit; // قد يكون الـ constructor توقف قبل FRuns
for i:= 0 to FRuns.Count- 1 do
TXLSXRichTextRun(FRuns[i]).Free;
FRuns.Clear;
end;
destructor TXLSXCells.Destroy;
begin
FPackedStorageEnabled:= False;
if FList<>nil then Clear; // FList تساوي nil إن فشل TList.Create
// ... التخزين المعبأ وجداول السلاسل وFList تحرر بـ Free
inherited Destroy;
end;
نقطة الفشل دقيقة: بعدّ تخصيصات الكومة أثناء البناء، الأولى هي النسخة نفسها؛ فإن فشل NewInstance لم يعمل جسم constructor ولم يستدع destructor، فتلك الحالة نظيفة دائمًا. والتخصيص الثاني هو TList: حين يفشل تبقى FRuns أو FList عند nil، ويستدعي RTL Destroy، وDestroy غير المحروسة تقرأ Count عبر مرجع nil، وهو بالضبط الـ access violation الثانوية. أما الإخفاقات اللاحقة داخل TXLSXCells.Create فغير مؤذية، لأنه ما إن توجد FList حتى يفحص Clear FPackedStorage <> nil قبل استخدام التخزين المعبأ
هذان الصنفان مهمان لأنهما يبنيان بالجملة: كل ورقة عمل محملة تبني مجموعة TXLSXCells، وكل خلية أو تعليق بمدايات منسقة يبني TXLSXRichText. والمصنف الكبير غني rich text يبني آلافًا منها، فتحت ضغط الذاكرة فإن للتخصيص الذي يفشل أخيرًا فرصة حقيقية أن يهبط على أحد تلك السطور
Free آمنة مع nil، والوصول إلى الحقل ليس كذلك
يفحص TObject.Free Self <> nil قبل استدعاء Destroy، فـ FRuns.Free على حقل nil لا تفعل شيئًا. أما قراءة FRuns.Count فليست كذلك: تقرأ Count حقلًا عند إزاحة من مؤشر النسخة، والاستدعاء الافتراضي مثل FRuns.Clear يقرأ أولًا مؤشر VMT من النسخة. وكلاهما يخطئ على nil. تلك اللامناظرية هي المصيدة كلها: استدعاءات التحرير في destructor نموذجي آمنة سلفًا؛ والحلقات واستدعاءات التنظيف الموضوعة قبلها هي موضع الشغب. هذا هو النمط في صنف صغير من صنعك:
type
TStyledText = class
private
FRuns: TList;
FIndex: TStringList;
public
constructor Create;
destructor Destroy; override;
end;
constructor TStyledText.Create;
begin
inherited Create;
FRuns := TList.Create; // EOutOfMemory هنا تترك FRuns = nil
FIndex := TStringList.Create; // ... وهنا تترك FIndex = nil
end;
destructor TStyledText.Destroy; // سيئ: يفترض أن الـ constructor انتهى
var
I: Integer;
begin
for I := 0 to FRuns.Count - 1 do // access violation حين FRuns = nil
TObject(FRuns[I]).Free;
FIndex.Free;
FRuns.Free;
inherited Destroy;
end;
الـ destructor المصحح يحرس كل مرجع ويحرر الحقول عبر استدعاءات آمنة مع nil فقط:
destructor TStyledText.Destroy;
var
I: Integer;
begin
if FRuns <> nil then
for I := 0 to FRuns.Count - 1 do
TObject(FRuns[I]).Free;
FreeAndNil(FIndex);
FreeAndNil(FRuns);
inherited Destroy;
end;
القواعد التي تنبثق عن هذا قصيرة بما يكفي للتطبيق في مراجعة كود:
- كل استخدام لحقل مملوك في destructor، سوى
FreeأوFreeAndNil، يحتاج فحصnilأوAssigned، بما فيها الاستدعاءات إلى مساعدات مثلClearأوClearCoreالتي تمشي في الحقل - حرر الكائنات المملوكة بـ
FreeأوFreeAndNil، ولا تستدعDestroyمباشرة أبدًا فهي تتخطى فحصnil - أبق
inherited Destroyالعبارة الأخيرة، فيهدم الجد حقوله الخاصة بعد زوال حقولك - لا تستدل من ترتيب الحقول في constructor؛ فالصائن التالي سيدرج تخصيصًا فوق تخصيصك وينقل نقطة الفشل بصمت
أين ينتمي inherited Create
استدع inherited Create أولًا، كما يفعل كلا constructorي HotXLS: فتوجد حقول الجد قبل أن يعمل كودك، وصورة الـ destructor المعكوسة، أي inherited Destroy آخرًا، تفك بالترتيب المعاكس. وإن رفع constructor الجد استثناء بنفسه، فلا يزال destructor الخاصك يعمل وكل حقولك nil، وهو ما يعالجه destructor آمن مع nil بلا حالات خاصة. والإسنادات الصريحة مثل FOwner := nil في constructor زائدة بعد InitInstance، لكنها غير مؤذية وأحيانًا تستحق البقاء توثيقًا
لا تلتقط وتحرر داخل constructor
نمطان حسنا النية يجعلان هذا أسوأ: الأول تغليف جسم constructor بـ try ... except Free; raise; end، فـ RTL ستستدعي destructor على أي حال فيدمر الكائن مرتين. والثاني ابتلاع الاستثناء "لنستمر": فيستلم المستدعي مرجعًا يبدو صالحًا لكائن حقوله nil، وينتقل الفشل إلى استدعاء تابع لا صلة له بعد وقت طويل. وإن أنشأ constructor مؤقتًا لا يحتفظ به، فحرر ذلك المؤقت في try ... finally محلي ودع الاستثناء يصعد دون مساس
كيف تختبر مسارات فشل constructor؟
أفشل كل نقطة تخصيص في constructor بقصد، واحدة تلو الأخرى، واشترط EOutOfMemory كل مرة، تليها بناء طبيعي ناجح. والأداة لذلك مدير ذاكرة بديل تعد GetMem وAllocMem وReallocMem الطلبات ذات الحجم الموجب وتعيد nil عند رقم ترتيبي مختار. عد تخصيصات constructor على بناء هدفك أولًا، ثم شغل حقن فشل واحد لكل رقم ترتيبي
بعد كل فشل محقون، ابن الصنف نفسه مجددًا بعد إعادة المخصص وجرّبه: في صنف rich text أضف مدى، واضبط نصه، وتأكد أن النص الصافي يعيد كما توقع. وفحوص دورة الحياة العادية تجاورها، مثل حفظ مصنف بمدايات منسقة وتعليقات إلى ملف وإلى stream، وتحرير المصنف المصدر، وإعادة الفتح ومقارنة النسختين
أعداد التخصيصات تعود لمترجم ومنصة واحدَين؛ يمكن لمترجم أو RTL آخر أن يخصص اختلافًا، فقس على البناء الذي تشحنه. يطبق HotXLS القاعدة الآمنة مع nil عبر المكتبة كلها، من مجموعات الأنماط والرسوم البيانية وpivot إلى جداول hash وكاتب zip ومصدّر PDF. وجداول hash تستحق ملاحظة خاصة: جدول يغير قناعه قبل إعادة تخصيص تفشل بعدها يترك destructorه يمشي في الذاكرة بعد الجدول القديم، فيجب أن يتغير القناع فقط بعد وجود التخزين الجديد. وحالة واحدة تجلس في RTL نفسها: TInterfaceList تخطئ في destructorها حين يفشل تخصيصها الداخلي. ولا يساوي أي من هذا تراجعًا لعملية اعتباطية تفشل في المنتصف، وليس ضمان استرداد عام لنفاد الذاكرة
ماذا يغير هذا للكود الذي يفتح مصنفات كبيرة؟
عمليًا، EOutOfMemory من تلك مسارات البناء إشارة تستطيع التقاطها والتصرف عليها، بدل access violation تضطر لمعاملتها انهيارًا. لا تأخذ TXLSXWorkbook.Create وسائط، وتعيد TXLSXWorkbook.Open(const FileName: WideString): Integer 1 عند النجاح و-1 عند الفشل. وتسجل الاستثناءات المرفوعة أثناء التحميل في مجموعة Diagnostics للمصنف كـ xlsDiagnosticUnhandledException ثم ترفع من جديد، وهو السلوك الموصوف في مقالة التشخيصات المهيكلة في HotXLS. وغلاف إعادة المحاولة بهذا الشكل:
type
EWorkbookOpenFailed = class(Exception);
function OpenWorkbookWithRetry(const FileName: WideString;
MaxAttempts: Integer): TXLSXWorkbook;
var
Attempt: Integer;
Book: TXLSXWorkbook;
begin
Result := nil;
for Attempt := 1 to MaxAttempts do
begin
Book := nil;
try
try
Book := TXLSXWorkbook.Create; // بلا وسائط constructor
if Book.Open(FileName) <> 1 then // 1 = نجاح، و-1 = فشل
raise EWorkbookOpenFailed.CreateFmt('HotXLS could not open %s',
[string(FileName)]);
Result := Book;
Book := nil; // الملكية تنتقل إلى المستدعي
Exit;
finally
Book.Free; // آمنة مع nil بعد التسليم
end;
except
on E: EOutOfMemory do
begin
if Attempt = MaxAttempts then
raise;
ReleaseApplicationCaches; // كودك: أسقط caches وكتبًا أخرى
end;
end;
end;
end;
بضع تفاصيل في ذلك الغلاف مقصودة: كل محاولة تبني TXLSXWorkbook جديدًا، لأن destructors الآمنة مع nil تجعل إخفاقات البناء نظيفة لكنها لا تجعل Open ناقص الإتمام قابلًا للاستئناف، فالنسخة الفاشلة تحرر ببساطة. ولا تعاد إلا EOutOfMemory؛ أما EWorkbookOpenFailed وأخطاء password وكل ما سواها فيصعد أول محاولة. ولا يجب إعادة EAccessViolation إطلاقًا، لأنها تشير إلى عيب لا إلى نقص مورد. وإعادة المحاولة لا معنى لها إلا إذا تغير شيء بين المحاولات، ولهذا يحرر المعالج الذاكرة أولًا. وإن احتجت أثر التشخيص للمحاولة الفاشلة، فاقرأ Book.Diagnostics قبل أن تحررها كتلة finally
إعادة المحاولة تخفيف لا خطة سعة: عملية 32-bit تصطدم بـ EOutOfMemory باستمرار على ملفات كبيرة تنقصها فضاء عناوين متصل غالبًا لا ذاكرة فيزيائية، والجواب الدائم بناء Win64. وينطبق التفكير نفسه على جهة الكتابة، حيث تبقي حفظات HotXLS الآمنة من الانهيار بملفات temp مرحلية حفظًا فاشلًا عن إتلاف الملف القائم. وإن تشاركت عدة threads مصنفًا واحدًا، فـ عقود القراءة وحراس الكتابة في HotXLS تصف كيف ينسق الوصول إلى تلك النسخة
قائمة تحقق لأصناف Delphi الخاصة بك
الحراسة نفسها سطر واحد لكل واحدة، والتدقيق نفسه يأخذ دقائق على كودك: ابحث عن destructors تستدعي تابع Clear أو Reset أو ClearCore قبل تحرير الحقول، ثم افحص هل يلمس ذلك التابع حقلًا ربما لم يبلغه constructor. وابحث عن حلقات على قوائم مملوكة لا يسبقها فحص nil. وابحث عن constructors تلتقط استثناءات، وأزل الابتلاع والـ Free اليدوي معًا. وإن أردت برهانًا لا تفقّدًا، فمدير الذاكرة العد الموصوف أعلاه يسع صفحة كود ويحول كل تخصيص في constructor إلى حالة اختبار
أما المزايا الكاملة وإصدارات Delphi وC++Builder المدعومة والترخيص، فراجع صفحة منتج مكوّن HotXLS لـ Excel في Delphi