مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C# ── أنماط تحرير مراجع COM وقرار الاستبدال

· آخر تحديث: · · Excel, C#, COM, .NET, .NET Framework, Office, صيانة الأصول القديمة, الاستشارات التقنية

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621607)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

غو كومورا (2026). مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C# ── أنماط تحرير مراجع COM وقرار الاستبدال. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621607 https://comcomponent.com/ar/blog/excel-com-interop-process-remains/

DOI (أحدث نسخة)
10.5281/zenodo.21621607
DOI (هذه النسخة)
10.5281/zenodo.22240993

«عندما بنيت ميزة لإخراج تقرير عبر Excel، وجدت صفّاً طويلاً من EXCEL.EXE في مدير المهام» أو «أنهيت التطبيق، لكن عند محاولة فتح الملف التالية ظهرت رسالة «مستخدم من عملية أخرى»» أو «تراكمت مئات عمليات Excel على خادم الدفعة الليلية، فاستنزفت الذاكرة وتوقّف كل شيء». كل من كتب شيفرة لتشغيل Excel عبر Microsoft.Office.Interop.Excel من C# يصطدم بهذه الظاهرة تقريباً مرّة واحدة على الأقل. ويصلنا نحن أيضاً بانتظام استفسار من نوع «أستدعي Quit() لكن Excel لا ينتهي».

المزعج في هذه المشكلة أنها تبدو وكأنها «تحدث أحياناً فقط». تختفي في جهاز التطوير لكنها تبقى في الإنتاج، وتبقى عند التشغيل التصحيحي لكنها تختفي عند الإصدار ── وبما أن شرط إعادة الإنتاج متذبذب، يميل الحل العرضي Process.Kill إلى التسلّل إلى شيفرة الإنتاج. لكن السبب ليس حظاً، بل يمكن تفسيره تماماً بآلية عدّ مراجع COM وآلية RCW (Runtime Callable Wrapper) في .NET. في هذا المقال نرتّب من منظور عملي: آلية البقاء، والفخ النمطي «قاعدة النقطتين»، وتصنيف نمطي التحرير وتوصية شركتنا، والطريقة الصحيحة لقتل العملية كملاذ أخير، وصولاً إلى قرار الاستبدال بمكتبات Open XML.

1. الخلاصة أولاً

في سطر واحد: Quit() ليس تحريراً، فلا تنتهي EXCEL.EXE حتى تُعاد جميع مراجع COM التي يمسك بها RCW. فيما يلي التفصيل والعلاج.

  • بقاء EXCEL.EXE ليس خللاً برمجياً، بل لأن جانب .NET ما زال ممسكاً بمرجع COM. Quit() ليس أكثر من طلب بمعنى «ينتهي عندما تُحرَّر جميع المراجع»، وطالما بقي مرجع ينتظر Excel بأمانة.
  • يتعامل .NET مع كائنات COM عبر غلاف يُسمّى RCW (Runtime Callable Wrapper)، ويستمر في الاحتفاظ بمرجع للكائن حتى يُجمع RCW بواسطة GC (أو يُحرَّر صراحة). 1
  • ربط نقطتين أو أكثر متتاليتين، كما في book.Worksheets[1].Range["A1"]، يولّد RCW لكائنات وسيطة دون أن تُوضع في أي متغيّر، فيحدث عدم تحرير. يُعرف هذا اصطلاحاً بـ«قاعدة النقطتين»، وهو الفاعل الرئيسي لهذه المشكلة.
  • يوجد اتجاهان للتحرير: (أ) تطبيق Marshal.ReleaseComObject على كل كائنات COM بانضباط، و(ب) حصر المراجع في متغيّرات وترك جمعها لـ GC (GC.Collect + WaitForPendingFinalizers). تصنّف الوثائق الرسمية ReleaseComObject بأنه «يُستخدم فقط عند الضرورة المطلقة». 2
  • توصية شركتنا هي جعل نمط GC الذي يعزل المعالجة في تابع واحد هو الأساس، وإدخال غلاف صغير منظّم عبر using للتحرير فقط عند الحاجة إلى التحكّم بترتيب التحرير (الفصل 4).
  • كضمان لحالات البقاء رغم كل ذلك، نأخذ مقبض النافذة من Application.Hwnd مسبقاً، ونحدّد PID عبر GetWindowThreadProcessId ثم ننفّذ Kill. لا تُستخدم طريقة التخمين عبر الفرق بين قائمة العمليات قبل التشغيل وبعده، لأنها قد تتسبّب في حادثة تشمل ملف Excel مفتوحاً لدى المستخدم.34
  • أساساً، أتمتة Office على جانب الخادم أو في بيئة غير مأهولة غير مدعومة من Microsoft. 5 إن كان الأمر توليد تقارير غير مأهول، ففكّر أولاً في الاستبدال بـ Open XML SDK / ClosedXML اللتين لا تشغّلان Excel (الفصلان 6-7).67

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 24، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. لماذا تبقى EXCEL.EXE ── عدّ المراجع وRCW

أولاً، المبدأ من جانب COM. يُدار عمر كائن COM بعدّ المراجع، ولا ينتهي خادم أتمتة Excel (EXCEL.EXE) حتى تُعاد جميع المراجع الممنوحة لعملاء خارجيين. وحتى في زمن VB6، كان نسيان استدعاء Release يترك العملية باقية بالطريقة نفسها (لمراجعة COM، انظر «ما هو COM / ActiveX / OCX»).

ثانياً، جانب .NET. لا تلمس شيفرة C# كائن COM مباشرة، بل تعمل عبر وكيل ينشئه CLR يُسمّى RCW. يُنشأ RCW واحد داخل العملية لكل كائن COM، ويخزّن مؤشّر واجهة COM مؤقّتاً، ويحرّر مرجع كائن COM عندما يُجمع هو نفسه بواسطة GC. 1 بعبارة أخرى، استُبدلت إدارة العمر من طريقة «عدّ المراجع بنفسك» إلى طريقة «تركها لـ GC».

بتركيب هاتين النقطتين، يُستنتج مباشرة سبب بقاء EXCEL.EXE. سلسلة المراجع تظهر في الشكل التالي.

داخل EXCEL.EXEداخل عملية .NETيستمر في الإمساك بمرجع COMهنا يعمل الأثريؤثر هنا لكنه لا يحررعد مراجع COMلا ينقص حتى تعود كل المراجع الممنوحة للخارجEXCEL.EXE لا تنتهيالمتغيراتexcel / book / sheetكائنات وسيطة مجهولةقيم عائدة مثل book.Worksheetsلا متغير فلا يمكن تحريرها يدوياً (الفصل 3)RCWواحد لكل كائن COMيعيد مرجع COM عند جمعه بـ GCMarshal.ReleaseComObjectأو الجمع عبر GC (الفصل 4)excel.Quit()يخبر فقط بأنه «يجوز الإنهاء»ولا ينقص أي مرجع

سواء جاء المسار من «المتغيّرات» أو من «الكائنات الوسيطة المجهولة» في اليسار، لا ينتهي الطرف الأيمن ما دام RCW ممسكاً بمرجع COM. ما يعمل هو السهم من REL إلى R من بين السهمين في أسفل الشكل فقط. بالمراحل يكون الأمر كالتالي.

المرحلة ما يحدث
new Excel.Application() تبدأ EXCEL.EXE بالتشغيل، ويُنشأ RCW لـ Application
عمليات الخلايا والحفظ وغيرها يتزايد RCW مع كل كائن يُلمس: Workbook، وWorksheet، وRange…
excel.Quit() مجرّد إخبار Excel بأنه «يجوز الإنهاء». لا ينقص أي مرجع مما يمسكه RCW
الخروج من التابع تختفي إشارة .NET إلى RCW، لكن RCW نفسه ما زال حياً في الكومة
GC (في وقت ما) يُجمع RCW، وعندها فقط يُعاد مرجع COM ← تنتهي EXCEL.EXE

هناك نقطتان أساسيتان. أولاً، Quit() ليس تحريراً. طالما بقي مرجع، ينتظر Excel. من الطبيعي أن ينقطع المرجع وينتهي Excel أيضاً عندما تنتهي عملية التطبيق تماماً، لكن في أشكال يبقى فيها العملية الأم حيّة، كالتطبيقات المقيمة أو تطبيقات الويب، لا يأتي ذلك «الوقت ما». ثانياً، توقيت التحرير غير محدَّد لأنه يعتمد على GC. إن كانت هناك سعة في الذاكرة، فقد لا يعمل GC لعشرات الدقائق، وخلال ذلك تبقى EXCEL.EXE كعملية زومبي. عدم إمكانية إعادة الإنتاج من نوع «تبقى أحياناً» أو «تبقى في الإنتاج فقط» ليس إلا تذبذب توقيت GC ظاهراً كما هو.

علماً بأن Excel المشغَّل بـ Visible = false لا يملك نافذة، فحتى إن بقي فلن يراه المستخدم. النمط النموذجي هو ظهور الأعراض كخطأ «الملف مستخدم» عند الحفظ الثاني، أو «بطء الجهاز»، ثم اكتشاف صف EXCEL.EXE في مدير المهام لأول مرّة. أول خطوة في التحقيق هي عدّ عددها عبر tasklist | findstr EXCEL.

3. الفخ النمطي «قاعدة النقطتين» ── الكائنات الوسيطة غير المرئية

في شيفرة الاستفسارات من نوع «أحرّر كل المتغيّرات بعناية لكنها تبقى»، توجد تقريباً دائماً أسطر مثل التالي.

// 一見きれいだが、解放できないRCWを生んでいる
excel.Workbooks.Open(path);
book.Worksheets[1].Range["A1"].Value2 = "hello";

ينشئ excel.Workbooks RCW لمجموعة Workbooks ويعيده. إن استدعيت .Open(...) دون استلام هذه القيمة المعادة في متغيّر، يبقى RCW الخاص بـ Workbooks في الكومة ككائن مجهول «حي لكن لا أحد يشير إليه». وبما أنه لا يوجد متغيّر، لا توجد وسيلة أصلاً لاستدعاء Marshal.ReleaseComObject عليه. السطر الثاني أشد خطورة، إذ ينشئ 3 كائنات RCW مجهولة في سطر واحد: Worksheets (مجموعة)، وWorksheets[1] (ورقة)، وRange["A1"] (نطاق).

القاعدة التجريبية لتجنّب ذلك هي «قاعدة النقطتين» المتداولة منذ القدم في مجتمع أتمتة Office. يمكن إعادة صياغتها بـ: لا تربط نقطتين أو أكثر متتاليتين على كائن COM. استلم كل كائن وسيط في متغيّر مرّة واحدة.

// すべての中間オブジェクトに名前を付ける
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(path);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";

قد يبدو هذا مطوَّلاً، لكنه أسلوب كتابة لـ«جعل كل ما يجب تحريره قابلاً للتعداد بالكامل». نذكر أيضاً أنماطاً مشتقّة يسهل إغفالها.

  • foreach: ينشئ foreach (Excel.Worksheet s in book.Worksheets) كائنات RCW للمجموعة والمعدّد، وكذلك لكل عنصر. في اتجاه ReleaseComObject، القاعدة المتّبعة هي استلام كل عنصر في متغيّر عبر حلقة for بفهرس.
  • القراءة والتجاهل داخل تعبير شرطي: يُنشأ RCW أيضاً داخل تعبير مثل if (excel.Workbooks.Count > 0).
  • معطيات تعبير مركّب: تعبير مثل sheets.Add(After: sheets[sheets.Count]) ينشئ عدّة RCW مجهولة في سطر واحد.
  • الاشتراك في الأحداث: عند ربط معالج بحدث Application أو Workbook، يحتفظ ذلك الارتباط بمرجع. ألغِ الاشتراك دائماً قبل الإنهاء.

أخيراً عن المصدر. تسمية «قاعدة النقطتين» نفسها من المجتمع، وليست مصطلحاً رسمياً من Microsoft. لكن مضمون القاعدة له سند من مصدر أولي. وثيقة دعم Microsoft التي تعالج مشكلة عدم إنهاء تطبيق Office بعد الأتمتة تضع في رأس شروط الإنهاء «إعلان كل كائن كمتغيّر جديد»، وتبيّن كتابة تتوسّط oBooks = oExcel.Workbooks بدل الربط المتتالي مثل oExcel.Workbooks.Add(). 8 إن وضعت هذا إلى جانب «يُنشأ RCW واحد لكل كائن COM ويحرّر المرجع عند جمعه بـ GC»1، يمكن تتبّع سبب تسرّب الكائنات الوسيطة من المصادر الأولية وحدها. الوثيقة نفسها تشير أيضاً إلى استدعاء GC.Collect وGC.WaitForPendingFinalizers إن لم ينتهِ بعد التحرير.8 اعتبر أن الاتجاهين في الفصل التالي امتداد لهذه الوثيقة.

4. تصنيف أنماط التحرير ── اتجاه ReleaseComObject واتجاه GC

يوجد أسلوبان لإنهاء EXCEL.EXE بشكل مضمون. كلاهما يعمل إن كُتب بشكل صحيح. المشكلة هي «هل يمكن الاستمرار في الكتابة الصحيحة؟»، وهنا يظهر الفرق من الناحية العملية.

4.1 (أ) اتجاه تطبيق Marshal.ReleaseComObject بانضباط

ينقص Marshal.ReleaseComObject عدّاد المراجع الداخلي لـ RCW، وعندما يصل إلى 0 يحرّر فوراً مرجع COM الذي يمسك به RCW.2 الميزة هي إمكانية التحرير في توقيت حتمي دون انتظار GC.

الشيفرة تطول، فانظر الهيكل أولاً. المطلوب هو «استلام كل ما استُخدم في متغيّر، والتحرير بعكس ترتيب الإنشاء»، لكن Close وQuit نفسهما استدعاءات COM فقد يفشلان ── لذلك النقطة هي تداخل try/finally حتى يصل التحرير اللاحق دائماً حتى لو وقع استثناء في الوسط.

try
    Application → Workbooks → Workbook → Sheets → Worksheet → Range  ← ترتيب الإنشاء
finally
    حرِّر Range و Worksheet و Sheets                                 ← عكس ترتيب الإنشاء
    try   book.Close()
    finally
        حرِّر Workbook و Workbooks
        try   excel.Quit()
        finally
            حرِّر Application

كتابته كما هو في C# يكون كالتالي. يبدو طويلاً لأن التداخل وفحص null مكتوبان بالكامل دون اختصار.

using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;

Excel.Application excel = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range cell = null;
try
{
    // لا تستخدم مهيِّئ الكائن (new ... { DisplayAlerts = false }).
    // إن فشل استدعاء COM في الـ setter يدخل finally و excel ما زال دون تعيين،
    // فلا يمكن Quit ولا التحرير لـ EXCEL.EXE الذي سبق تشغيله
    excel = new Excel.Application();
    excel.DisplayAlerts = false;
    books = excel.Workbooks;
    book = books.Open(templatePath);
    sheets = book.Worksheets;
    sheet = (Excel.Worksheet)sheets[1];
    cell = sheet.Range["A1"];
    cell.Value2 = "hello";
    book.SaveAs(outputPath);
}
finally
{
    // حرِّر بعكس ترتيب الإنشاء. Close و Quit نفسيهما استدعاءات COM فقد يفشلان،
    // لذا تداخل try/finally حتى يُبلَغ Quit والتحرير حتماً حتى لو وقع استثناء في الوسط
    if (cell   != null) Marshal.ReleaseComObject(cell);
    if (sheet  != null) Marshal.ReleaseComObject(sheet);
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    try
    {
        if (book != null) book.Close(SaveChanges: false);
    }
    finally
    {
        if (book  != null) Marshal.ReleaseComObject(book);
        if (books != null) Marshal.ReleaseComObject(books);
        try
        {
            if (excel != null) excel.Quit();
        }
        finally
        {
            if (excel != null) Marshal.ReleaseComObject(excel);
        }
    }
}

كما هو واضح، ضعف هذه الطريقة هو ارتفاع تكلفة الانضباط. يجب استلام كل كائن COM جرى لمسه في متغيّر دون استثناء، والتحرير بترتيب عكسي يشمل مسار الاستثناءات أيضاً. وبالأخذ في الاعتبار أن Close أو Quit نفسهما قد يفشلان (خطأ COM، أو مصنّف مفصول، أو Excel لا يستجيب)، يجب أيضاً ضمان «الوصول إلى التحرير اللاحق حتى لو فشل شيء في الوسط» عبر try/finally متداخلة كما في المثال أعلاه ── وهذا مطلب صعب جداً بحسب الخبرة أن يلتزم به كل الأعضاء في كل تعديل. حتى تعبير مركّب واحد بنقطتين يتسلّل في مكان واحد فقط يعيد إحياء المشكلة.

والأهم من ذلك أن الوثائق الرسمية نفسها تحذّر من سوء الاستخدام. تنص مرجعية Marshal.ReleaseComObject على أنه وسيلة لحالات الحاجة إلى تحرير الموارد في وقتها أو حين يكون لترتيب التحرير معنى، و«يُستخدم فقط عند الضرورة المطلقة (use the ReleaseComObject only if it is absolutely required)».2 بما أن RCW آلية تُشارَك كواحد لكل كائن COM داخل العملية، فإن كان مكان آخر في الشيفرة ما زال يستخدم RCW حرّره مكان آخر بالفعل، يؤدّي ذلك إلى InvalidComObjectException، أو في أسوأ الحالات، إلى انتهاك وصول وتلف ذاكرة العملية.2 في بنية تشترك فيها عدّة وحدات داخل التطبيق في تشغيل Excel، تحدث هذه الحادثة فعلياً.

علماً بأنه عند تمرير نفس مؤشّر الواجهة إلى CLR عدّة مرّات، قد يتجاوز عدّاد مراجع RCW 1، وفي تلك الحالة لا يُحرَّر باستدعاء واحد. يوجد أيضاً Marshal.FinalReleaseComObject الذي يجعل العدّاد 0 قسراً2، لكن لحظة الحاجة إلى هذا الـ API هي إشارة على عدم فهم إدارة العمر، لذا توصي شركتنا بإعادة النظر في التصميم.

تنبيه أمني واحد على محور مختلف عن بقاء العملية. المصنّف الذي فُتح بـ Workbooks.Open عبر أتمتة COM قد يشغّل VBA دون تحذير ماكرو. عيّنة هذا المقال تفترض فتح قالب موثوق يديره تطبيقك، لكن إن كان من المحتمل فتح مصنّفات من مصدر خارجي كملفات مجلد مشترك أو ملفات رفعها المستخدم، اضبط excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable (فضاء الأسماء Microsoft.Office.Core) قبل Open لتعطيل الماكرو قسراً.9 هذا يسد هجوماً من نوع «استبدال القالب يتحوّل مباشرة إلى تنفيذ شيفرة عشوائية». ينطبق هذا التنبيه أيضاً على عيّنة نمط GC الموضَّحة لاحقاً.

4.2 (ب) اتجاه حصر المراجع وتركها لـ GC ليجمعها

الأسلوب الآخر هو ترك تحرير RCW لـ GC وفق الآلية الأصلية، وتشغيل ذلك الـ GC في توقيت محدَّد. بما أن RCW يحرّر مرجع COM عند جمعه بواسطة GC1، فبإجراء «GC كامل + انتظار اكتمال الـ finalizer بعد خروج جميع المراجع التي تلمس Excel من النطاق»، يمكن إنهاء EXCEL.EXE دون كتابة ReleaseComObject ولو مرّة واحدة.

using System.Runtime.CompilerServices;
using Excel = Microsoft.Office.Interop.Excel;

public void ExportReport(string templatePath, string outputPath)
{
    try
    {
        // Excelを触る処理は別メソッドに完全に隔離する
        ExportReportCore(templatePath, outputPath);
    }
    finally
    {
        // 例外経路こそEXCEL.EXEが残りやすいので、必ずfinallyで実行する。
        // メソッドを抜けた後なら、RCWへの参照はどこにも残っていない
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();   // ファイナライザーが切り離したRCWを回収する2周目
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath)
{
    var excel = new Excel.Application();
    try
    {
        excel.DisplayAlerts = false;
        Excel.Workbooks books = excel.Workbooks;
        Excel.Workbook book = books.Open(templatePath);
        Excel.Sheets sheets = book.Worksheets;
        Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
        Excel.Range cell = sheet.Range["A1"];
        cell.Value2 = "hello";
        book.SaveAs(outputPath);
        book.Close(SaveChanges: false);
    }
    finally
    {
        excel.Quit();
    }
}

شروط النجاح ثلاثة.

  1. عزل الشيفرة التي تلمس Excel في تابع واحد. طالما أبقى JIT المرجع حياً على المكدّس، لا يستطيع GC جمعه، لذا يجب استدعاء GC دائماً «خارج التابع الذي لمس Excel». الغرض من NoInlining هو منع إبطال العزل بسبب التوسيع المضمَّن.
  2. عدم تسريب RCW إلى حقل أو قيمة معادة. إن تسرّب ولو واحد خارج التابع، يبقى Excel طالما ظلّت تلك الإشارة حيّة.
  3. اجعلها ثلاثية GC.CollectGC.WaitForPendingFinalizersGC.Collect. بما أن تنظيف RCW يتم عبر الـ finalizer، فالقاعدة المتّبعة هي دورتان: الاكتشاف في Collect الأولى، وانتظار اكتمال الـ finalizer، وجمع البقايا في Collect الثانية.

تنبيه: عند التشغيل بمصحّح مرفَق، يُمدَّد عمر المتغيّر حتى نهاية التابع، فقد لا يُجمع RCW حتى بهذه الطريقة. هذه هي حقيقة ظاهرة «تبقى في التصحيح لكن تختفي في الإصدار»، فأجرِ التحقّق من العمل دائماً ببناء Release ودون مصحّح.

ميزة هذه الطريقة هي عدم حدوث تسريب حتى مع خرق قاعدة النقطتين. يجمع GC معاً كل ما انقطعت إشارته، بما في ذلك RCW الوسيطة المجهولة. تتركّز نقطة المراجعة في سؤال واحد: «هل المعالجة التي تلمس Excel محصورة في هذا التابع؟»، فتنخفض تكلفة الانضباط انخفاضاً كبيراً. أما العيب فهو رائحة شيفرة ناتجة عن الاستدعاء الصريح لـ GC.Collect (توقّف مؤقّت بسبب GC كامل للتطبيق بأكمله)، وخطر أن يكسرها عضو لا يعرف السبب أثناء إعادة هيكلة حسنة النيّة. اترك دائماً تعليقاً يوضّح السبب على الثلاثية.

4.3 توصية شركتنا ── العزل + GC كأساس، وغلاف منظّم عند الحاجة

نقارن بين الاتجاهين من منظور عملي.

  (أ) اتجاه ReleaseComObject (ب) اتجاه GC
توقيت التحرير حتمي (لحظة الاستدعاء) شبه حتمي (لحظة ثلاثية GC)
تكلفة الانضباط مرتفعة. يجب أن يلتزم الجميع بتحويل كل كائن إلى متغيّر والتحرير بترتيب عكسي منخفضة. يكفي الالتزام بعزل التابع فقط
أعراض المبالغة InvalidComObjectException، وانتهاك وصول2 توقّف مؤقّت بسبب GC القسري
الاتساق مع الإرشاد الرسمي «فقط عند الضرورة المطلقة»2 يتّسق مع إدارة عمر RCW الأصلية (تركها لـ GC)1
الحالات المناسبة عندما يكون لترتيب التحرير معنى. استخدام Excel بشكل متكرّر ودقيق في عملية طويلة العمر معظم معالجات التقارير التي تكتمل بـ«فتح وكتابة وإغلاق» في مكان واحد

توصية شركتنا هي جعل نمط العزل + GC (ب) هو الأساس. من الطبيعي أن تنحصر معالجة مثل إخراج التقارير في تابع واحد، وبمجرّد اتّخاذ ذلك الشكل، لا يحدث تسريب هيكلياً. كما يُتجنَّب خطر سوء استخدام ReleaseComObject، ويتّسق ذلك مع الموقف الرسمي.

استخدم (أ) فقط عندما تكون آنية التحرير وترتيبه ضروريين فعلاً، وفي تلك الحالة، لا تسمح بكتابة ReleaseComObject الخام، بل أدخل غلافاً صغيراً منظّماً عبر using.

using System.Runtime.InteropServices;

/// <summary>COMオブジェクトをusingスコープで解放するラッパー</summary>
public readonly struct ComScope<T> : IDisposable where T : class
{
    public T Value { get; }
    public ComScope(T value) => Value = value;

    public void Dispose()
    {
        if (Value is not null && Marshal.IsComObject(Value))
            Marshal.ReleaseComObject(Value);
    }
}
using var books = new ComScope<Excel.Workbooks>(excel.Workbooks);
using var book  = new ComScope<Excel.Workbook>(books.Value.Open(templatePath));
using var sheets = new ComScope<Excel.Sheets>(book.Value.Worksheets);
using var sheet = new ComScope<Excel.Worksheet>((Excel.Worksheet)sheets.Value[1]);
using var cell  = new ComScope<Excel.Range>(sheet.Value.Range["A1"]);
cell.Value.Value2 = "hello";
book.Value.SaveAs(outputPath);
book.Value.Close(SaveChanges: false);

بما أن Dispose يُنفَّذ بترتيب عكسي لترتيب إعلان using، فإن «التحرير بترتيب عكسي للإنشاء» مضمون عبر آلية اللغة نفسها. تزداد الكتابة بمقدار .Value، لكن يمكن ضغط الانضباط إلى قاعدة واحدة: «كائن COM يُستلم دائماً عبر ComScope». وبالمقابل، إن كُتب تعبير مركّب مثل books.Value.Open(...).Worksheets يعود التسريب من جديد، لذا يبقى تعليم قاعدة النقطتين ضرورياً على أي حال.

تنبيهان مشتركان في كلا النمطين. أولاً، لا تجعل مربّع حوار تأكيد الحفظ يظهر قبل Quit(). اضبط DisplayAlerts = false وClose(SaveChanges: false) صراحة. إن تجمّد Excel المخفي بانتظار مربّع حوار، لن يكتمل Quit نفسه. ثانياً، بما أن COM الخاص بـ Excel يفترض STA، لا تلمس نفس Application من عدّة خيوط. نُظّمت العلاقة بين الخيوط وشقّة COM في «أساسيات STA/MTA في COM».

4.4 خطوات التحقّق من أن المشكلة زالت

بعد إدخال نمط التحرير، حدّد أيضاً خطوات التحقّق من «هل اختفت فعلاً؟». شروط إعادة الإنتاج متذبذبة، فإن لم تُثبَّت الخطوات تُخطئ في عدّ «اختفت مرّة بالمصادفة» نجاحاً.

  1. نفّذ ببناء الإصدار ومن دون مصحّح. كما في القسم 4.2، إن كان المصحّح مرفقاً يمتد عمر المتغيّر حتى نهاية التابع، فلا يُجمع RCW حتى في نمط GC المكتوب بشكل صحيح. من Visual Studio استخدم «بدء بدون تصحيح».
  2. عدّ EXCEL.EXE قبل البدء. نفّذ tasklist /FI "IMAGENAME eq EXCEL.EXE" واحفظ عدد نُسخ Excel الجارية (بما فيها ما فتحه المستخدم يدوياً). في PowerShell يكفي @(Get-Process EXCEL -ErrorAction SilentlyContinue).Count للحصول على العدد فقط.
  3. نفّذ المعالجة نفسها 10 مرّات متتالية على الأقل. مرّة واحدة قد تختفي بالمصادفة حسب توقيت GC.
  4. انتظر بضع ثوانٍ بعد انتهاء المعالجة ثم عدّ مرّة أخرى. النجاح هو العودة إلى نفس عدد الخطوة 2. إن زاد، فالزيادة هي عدد مرّات التسرّب.
  5. أعد الخطوات نفسها على مسار الاستثناء. أفشل عمداً في الوسط: مسار قالب غير موجود، أو وجهة حفظ للقراءة فقط، وتحقّق من عودة العدد رغم ذلك. معظم حوادث بقاء EXCEL.EXE تحدث في مسار الاستثناء لا في المسار السليم، فتجاوز هذه الخطوة يعني أنك لم تتحقّق.
  6. إن كان تطبيقاً مقيماً، عدّ دون إنهاء التطبيق. إن انتهت العملية تنقطع المراجع ويسقط Excel أيضاً، فالعد بعد إغلاق التطبيق لا يختبر شيئاً (الفصل 2).

إن بقيت أعداد الخطوتين 2 و4 في السجل، تكتشف لاحقاً تراجع شيفرة التحرير. إن كنت قد وضعت Kill التأمين في الفصل 5، لا تقول «التحرير يعمل بشكل صحيح» إلا بعد التحقّق أيضاً من أن Kill لم يُفعَّل (عدم ظهور سجل التحذير عند التفعيل).

5. الملاذ الأخير ── تحديد PID من Hwnd والتخلّص المؤكّد

حتى مع تنفيذ نمط التحرير بشكل صحيح، تبقى حالات مثل «لا ينتهي Excel بسبب إضافة» أو «تبقى عملية واحدة حتماً في مسار استثناء شاذ». في الدفعات غير المأهولة، تتسبّب العملية المتبقّية الواحدة في قفل ملف لمهمّة اليوم التالي، لذا يستحق الأمر تجهيز Kill للعملية كضمان أخير. المشكلة هي طريقة تحديد «أي EXCEL.EXE يُقتل».

الخطأ الشائع هو طريقة التحديد عبر الفرق في قائمة العمليات قبل التشغيل وبعده. مقارنة Process.GetProcessesByName("EXCEL") قبل التشغيل وبعده، واعتبار الزيادة نسخته الخاصة ── هذا هش أمام التزامن. فإن فتح المستخدم Excel يدوياً أثناء أخذ الفرق، يحدث كشف خاطئ، وإن نُفّذت مهام بنفس الطريقة بالتوازي، تتبادل الخلط فيما بينها. أسوأ حادثة لهذه الطريقة هي قتل ملف Excel غير محفوظ يعدّله المستخدم وضياع البيانات، وهذا فعلاً وصلنا كاستفسار من عميل.

الطريقة الصحيحة للتحديد هي الحصول على مقبض النافذة العلوية عبر خاصية Hwnd الخاصة بكائن Application الذي شغّلته بنفسك، والحصول على معرّف العملية التي أنشأت تلك النافذة عبر GetWindowThreadProcessId من Win32 API.34 بما أن المقبض خاص بنسختك، لا يوجد مجال للخلط مع Excel آخر.

using System.Diagnostics;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;

internal static class NativeMethods
{
    [DllImport("user32.dll")]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
}

public void ExportReport(string templatePath, string outputPath)
{
    // out引数なら、Excel操作の途中で例外が飛んでもハンドルが呼び出し元に残り、
    // 例外経路(保険が本当に必要な経路)でもGCとKillに必ず到達する
    Process excelProcess = null;
    try
    {
        ExportReportCore(templatePath, outputPath, out excelProcess);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        if (excelProcess != null)
        {
            KillIfStillAlive(excelProcess);   // تأمين لا يفعل شيئاً إن انتهى بشكل طبيعي
            excelProcess.Dispose();
        }
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath, out Process excelProcess)
{
    excelProcess = null;
    var excel = new Excel.Application();
    // بعد نجاح التشغيل، أحِط فوراً بـ try/finally حتى يُبلَغ Quit()
    // حتماً حتى لو فشل الحصول على Hwnd أو فتح المقبض
    try
    {
        // بعد Quit() تختفي النافذة ولا يمكن الحصول عليها، لذا خذها فور التشغيل.
        // GetProcessById يربط PID فقط، ومقبض عملية نظام التشغيل
        // يُفتح بتأخير عند أول وصول مثل WaitForExit / Kill. لذلك
        // المس SafeHandle أثناء حياة Excel لفتح المقبض هنا قسراً
        // (طالما بقي المقبض مفتوحاً، لا يُعاد استخدام هذا الـ PID في عملية أخرى)
        NativeMethods.GetWindowThreadProcessId((IntPtr)excel.Hwnd, out uint pid);
        excelProcess = Process.GetProcessById((int)pid);
        _ = excelProcess.SafeHandle;

        excel.DisplayAlerts = false;
        // …… عملية Excel ……
    }
    finally
    {
        excel.Quit();
    }
}

private void KillIfStillAlive(Process excelProcess)
{
    if (!excelProcess.WaitForExit(5000))   // في المسار الطبيعي يختفي خلال ثوانٍ
    {
        logger.LogWarning("لن يُنهَى EXCEL.EXE (PID {Pid}) لذا سيُفرَض إنهاؤه", excelProcess.Id);
        excelProcess.Kill();
    }
}

نقاط الانتباه في التنفيذ.

  • خذ Hwnd فور التشغيل مباشرة. بعد Quit() تُتلف النافذة ولا يمكن الحصول عليها. يمكن الحصول على Application.Hwnd حتى مع Visible = false.3
  • Kill هو «ضمان بعد إجراء التحرير بشكل صحيح». إن اعتمدت عليه من البداية، لا يُنفَّذ تنظيف ملفات Excel المؤقّتة، ما يؤدّي إلى أمور مثل تراكم ملفات الاستعادة التلقائية عند التشغيل التالي. الترتيب يجب أن يكون دائماً «التحرير ← Quit ← الانتظار ← Kill فقط إن بقيت حيّة».
  • انتبه لإعادة استخدام PID. بما أن PID في Windows يُعاد استخدامه، فإن كان التصميم يكتفي بتذكّر رقم PID ثم حلّه لاحقاً، يوجد خطر قتل عملية غير ذات صلة إن انتهى Excel خلال ذلك وأُسند نفس PID إلى عملية أخرى. انتبه إلى أن مجرّد استدعاء Process.GetProcessById هنا غير كافٍ. بما أن مقبض عملية نظام التشغيل يُفتح بتأخير عند أول وصول كـ WaitForExit / Kill وما شابه، فإنه لا يمكن منع الخلط إلا بلمس SafeHandle صراحة لفتح المقبض أثناء حياة Excel كما في الشيفرة أعلاه (طالما بقي المقبض مفتوحاً، لا يُعاد استخدام ذلك الـ PID).
  • ما يغطّيه هذا الضمان هو حالة عودة التابع مع بقاء EXCEL.EXE. إن تجمّد استدعاء COM نفسه كـ Workbooks.Open أو SaveAs أو Quit (بانتظار مربّع حوار نمطي خفي أو استجابة إضافة)، لن يُصل إلى finally، ولن يُفعَّل هذا الـ Kill أيضاً. فكّر في إجراء من طبقتين. أولاً، سدّ عامل مربّع الحوار عبر DisplayAlerts = false وAutomationSecurity المذكور آنفاً. وفوق ذلك، في الدفعات غير المأهولة، اجعل المهمّة التي تلمس Excel عملية منفصلة، وضع حدّاً زمنياً من الخارج يمكّن من Kill كامل (كإعداد «الوقت حتى الإيقاف» في Task Scheduler، أو Kill للعملية الفرعية من العملية الأم). بما أن المهلة الزمنية داخل العملية لا يمكنها مقاطعة استدعاء COM المتجمّد، فوضع الحد على مستوى العملية هو الأضمن.
  • عند تفعيل Kill، سجّله دائماً في السجل وراقب تواتره. إن زاد التفعيل، فهو إشارة على تراجع في شيفرة التحرير، أو على قرار الاستبدال في الفصل 7.

6. أساساً ── أتمتة Office على جانب الخادم غير مدعومة

يمكن السيطرة على بقاء EXCEL.EXE في تطبيقات العميل تقريباً بالتقنيات حتى هذه النقطة. لكن، فيما يخص الموضع الذي تتفاقم فيه هذه المشكلة أكثر ما يكون ── أتمتة Excel على خادم أو في بيئة غير مأهولة ── يوجد موقف رسمي ينبغي التحقّق منه قبل التقنية.

تنص Microsoft صراحة في وثيقة بعنوان «Considerations for server-side Automation of Office» على أنها لا توصي، ولا تدعم، أتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعلية (بما فيها ASP، وASP.NET، وDCOM، وخدمات NT).5 صُمّم Office على افتراض وجود مستخدم تفاعلي، وافتراضات لا تشكّل مشكلة على سطح المكتب ── كإظهار مربّع حوار تأكيد عند الخطأ وانتظار الاستجابة، ومكوّنات تفترض ملف تعريف المستخدم المنفّذ، وبنية غير قابلة لإعادة الدخول مبنية على STA ── تُظهر أنيابها كلّها على خدمات أو عمليات عاملة في IIS.10 استفسارات مثل «عند التشغيل من خدمة Windows، لا يعود Open في الإنتاج» أو «تجمّد بانتظار مربّع حوار مرّة في الشهر على IIS» سببها كلّها هذا التكوين غير المدعوم. بقاء EXCEL.EXE في هذا التكوين ليس مجرّد مشكلة تنظيف، بل يصبح مشكلة تشغيلية تتراكم فيها العمليات المتجمّدة إلى ما لا نهاية.

ما تذكره Microsoft كبديل هو تحرير تنسيق ملف Open XML الذي يعالج الملف مباشرة دون تثبيت Office أو تشغيله، وكذلك Microsoft Graph API الذي يعالج من جهة السحابة.10 Open XML SDK هو مكتبة من إنتاج Microsoft تستطيع قراءة وكتابة تنسيقات ملفات Office (كـ .xlsx) المقنَّنة كمعيار ECMA-376 / ISO/IEC 29500 عبر أصناف منمَّطة بقوة، ومبنية فوق ZIP وXML، فلا تحتاج إلى Excel نفسه.6 تختفي معاً مشاكل بقاء العملية والترخيص والتكوين غير المدعوم.

لكن Open XML SDK مكتبة «تحرّر تنسيق الملف مباشرة»، ولا توفّر سلوك تطبيق Excel نفسه. تنص اعتبارات التصميم الرسمية صراحة على أنها لا توفّر سلوكيات التطبيق كإعادة حساب الصيغ وتحديث البيانات، ولا وظيفة التحويل إلى تنسيقات أخرى (كـ PDF).11 كما أن الـ API أمين لبنية تنسيق الملف، فحتى كتابة خلية واحدة بالـ SDK الخام تتطلّب فهم بنية SpreadsheetML. ما يسد هذه الفجوة هو ClosedXML، وهو برمجيات مفتوحة المصدر برخصة MIT تضع فوق Open XML API واجهة حدسية بمفاهيم «مصنّف وورقة وخلية». يمكنها التعامل مع .xlsx / .xlsm دون تثبيت Excel (لا تشمل التنسيق القديم .xls).7 كتبنا بالتفصيل عن التمييز بين الاستخدامين وتصميم أسلوب القوالب في سياق إخراج التقارير في «كيفية بناء إخراج تقارير Excel».

علماً بأن Microsoft 365 لديها ترخيص لـ RPA غير المأهول (unattended license)، لكنه يجعل التنفيذ غير المأهول ممكناً من الناحية الترخيصية فقط، والسلوك يبقى “AS IS” ── أي أن التصنيف هو «استوعب على جانب التطبيق أي سلوك غير متوقَّع ناتج عن استخدام خارج التصميم».10 هذه النقطة يُساء فهمها كثيراً، فانتبه إلى أنه ليس «شراء الترخيص يجعله تكويناً مدعوماً».

7. جدول القرار ── الاستمرار في استخدام COM، أم الاستبدال بمنظومة Open XML

بناءً على ما سبق، يمكن تنظيم «الاستمرار في تشغيل Excel عبر COM، أم التخلّي عنه» في ثلاثة خيارات كالتالي.

  (1) الاستمرار في استخدام COM Interop (تغليف ومنظّم) (2) الاستبدال بـ Open XML SDK / ClosedXML (3) إعادة النظر في التصميم نفسه (Graph وغيره)
Excel نفسه ضروري (والترخيص لكل بيئة تشغيل أيضاً) غير ضروري غير ضروري
التنفيذ غير المأهول / على الخادم تكوين غير مدعوم5 لا مشكلة (البديل الموصى به)10 لا مشكلة
خطر بقاء العملية موجود (يُدار بتقنيات هذا المقال) غير موجود (لا تُشغَّل عملية) غير موجود
تنفيذ الماكرو (VBA) ممكن غير ممكن (والحفاظ عليه أيضاً يعتمد على المكتبة ── انظر 2 أدناه) غير ممكن
إعادة حساب الصيغ / الطباعة / التحويل إلى PDF ممكن غير ممكن11 متوفّر جزئياً في Graph
التفاعل مع Excel المفتوح لدى المستخدم ممكن غير ممكن غير ممكن
التنسيق القديم .xls (BIFF) يمكن القراءة والكتابة غير ممكن (.xlsx / .xlsm فقط)7 ──
سرعة التنفيذ / التوازي بطيء. يلزم فصل النسخ للتوازي10 سريع. يمكن التوازي كمكتبة اعتيادية يعتمد على الشبكة

محاور القرار هي أربعة أسئلة.

  1. هل يلزم التفاعل مع Excel أمام المستخدم مباشرة؟ لا يمكن بناء ميزة «الكتابة في مصنّف مفتوح لدى المستخدم، وتسليم استمرار العملية» إلا عبر COM Interop. في هذه الحالة، (1) هو الخيار الوحيد، فاستثمر في التنظيم الموضَّح في الفصل 4. تطبيقات سطح المكتب التفاعلية لا تندرج ضمن التكوين غير المدعوم أيضاً.
  2. هل تلزم ميزات تطبيق Excel نفسه (تنفيذ ماكرو، إعادة حساب، طباعة، إخراج PDF)؟ لا يمكن استبدال هذه بمنظومة Open XML.11 الجمع مع التنفيذ غير المأهول هو النمط الأصعب، ففكّر أولاً فيما إذا كان بالإمكان تعديل جانب المتطلّبات (نقل منطق الماكرو إلى C#، أو كتابة قيم محسوبة مسبقاً، وغيرها). ما يجب الانتباه إليه هو «الحفاظ» على القالب المحتوي على ماكرو (.xlsm). عمليات Open XML SDK منخفضة المستوى تحافظ عليه طالما لم تلمس جزء مشروع VBA، لكن مكتبات عالية المستوى مثل ClosedXML تحمّل المصنّف إلى نموذج كائنات وتعيد بناء الحزمة عند الحفظ، فقد يُفقد مشروع VBA. عند ترحيل قالب يحتوي ماكرو إلى منظومة Open XML، اجعل التحقّق الإلزامي باستخدام القالب الفعلي من أن «الماكرو يبقى ويعمل حتى بعد الفتح والكتابة والحفظ»، وإن تعذّر ضمان ذلك، أبقِ ذلك التقرير فقط على COM. يمكن استخدام حكم «ما هو VBA» مباشرة في التعامل مع أصول VBA.
  3. هل هو تنفيذ غير مأهول؟ إن كان التنفيذ من خدمة، أو Task Scheduler، أو تطبيق ويب، فالإجابة الافتراضية هي (2). معظم متطلّبات توليد التقارير هي مجرّد «إنشاء .xlsx بصبّ القيم والتنسيق»، وهذا يكتمل بـ ClosedXML.
  4. ما تنسيق الإدخال والإخراج؟ إن كان يلزم معالجة ملف .xls الوارد من شريك تجاري كما هو، لا يمكن استخدام منظومة Open XML. فكّر فيما إذا كان يمكن إدراج تحويل إلى .xlsx عند نقطة الاستقبال.

ما تقترحه شركتنا غالباً في المشاريع الفعلية هو التقسيم: «التوليد بـ ClosedXML، وعزل المعالجة التي تحتاج Excel نفسه فعلاً حتماً فقط في COM». نُفّذ توليد مئات التقارير يومياً بـ ClosedXML على الخادم، ونُنفَّذ فقط «تحديث المصنّف المحتوي على ماكرو» الشهري كمعالجة COM بضغط زر على سطح مكتب الموظّف المسؤول ── بهذا يختفي COM من البيئة غير المأهولة، ويندرج جزء COM المتبقّي ضمن تكوين مدعوم لأنه تطبيق تفاعلي. هذا نهج أكثر واقعية من إعادة الكتابة الشاملة، ويمكن التخلّص فيه من الأجزاء الأعلى خطراً بالترتيب.

8. نقاط الانتباه في عصر .NET (Core)

ننظّم، في حدود ما أمكن التحقّق منه، نقاط الانتباه عند الاستمرار في تشغيل Excel عبر COM في تطبيق رُحّل من .NET Framework إلى .NET (.NET 6/8 وغيرها).

  • يبقى التشغيل التبادلي مع COM حكراً على Windows. يعمل .NET أيضاً على Linux، لكن دعم التشغيل التبادلي مع COM المدمج مقتصر على Windows.12 اجعل الهدف صريحاً كـ net8.0-windows في المشروع الذي يتضمّن تشغيل Excel، ولا تتوقّع منصّات متعدّدة. عدم القدرة على التحميل في حاوية Linux يرتبط مباشرة بقرار الاستبدال في الفصل 7 (ClosedXML يعمل في حاوية Linux).
  • الأساس في طريقة الإشارة هو «مرجع COM + تضمين الأنواع التبادلية». عند إضافة Microsoft Excel Object Library كمرجع COM في Visual Studio، يُستخدم افتراضياً تضمين الأنواع التبادلية (Embed Interop Types). بما أنه لا يُضمَّن في تجميعتك إلا الأنواع المستخدمة، لا حاجة إلى توزيع PIA (Primary Interop Assembly) في بيئة التشغيل، وتزداد المتانة أمام اختلاف إصدارات Office.13
  • ما زال بإمكانك استخدام dynamic والمعطيات الاختيارية. ميزات C# الموجّهة للتشغيل التبادلي مع Office (المعطيات المسمّاة والاختيارية، وتبسيط استدعاء COM عبر dynamic) مدعومة أيضاً في .NET الحالي.13 لكن الكتابة بـ dynamic تجعل RCW المجهولة أصعب رؤية، لذا نوصي بالكتابة بأنواع صريحة في الشيفرة التي يُراد تنظيم تحريرها.
  • انتبه لواجهات برمجية تفترض منصّة معيّنة. تحمل واجهات متعلّقة بـ COM مثل Marshal.ReleaseComObject سمة خاصّة بـ Windows فقط، وعند استدعائها من مشروع متعدّد المنصّات تصبح هدفاً لتحذير المحلّل CA1416. يسهل إدارتها بفصل تشغيل Excel في مشروع مستقل.
  • الاتجاه المعاكس (استدعاء .NET من VBA) موضوع مختلف. ما زال ممكناً تكوين يعرض DLL خاص بـ .NET 8 كـ COM ويُستخدم من VBA، ولخّصنا الخطوات في «كيفية استخدام DLL خاص بـ .NET 8 من VBA مع الأنواع». بقلب التكوين من «تشغيل Excel من C#» إلى «استدعاء منطق C# من ماكرو Excel»، تُترك إدارة عمر العملية لـ Excel نفسه، فقد تختفي مشكلة البقاء هيكلياً في بعض الحالات.

خلاصة القول، حتى في عصر .NET (Core)، تبقى طريقة كتابة تشغيل Excel عبر COM ومطبّاته شبيهة تقريباً بعصر .NET Framework. ما تغيّر هو التصريح الصريح بكونه خاصاً بـ Windows فقط، وميل المرجع نحو تضمين الأنواع التبادلية، أما نقاش RCW وأنماط التحرير (الفصول 2-5) فيبقى سارياً كما هو.

9. الخلاصة

مشكلة بقاء EXCEL.EXE لا تعود ظاهرة متروكة للحظ إن فُهم الجسر بين إدارتي عمر: «يعيش COM بعدّ المراجع، ويموت RCW في .NET بواسطة GC». نلخّصها في قائمة تحقّق كالتالي.

  • Quit() ليس تحريراً. لا ينتهي Excel حتى تُعاد جميع مراجع COM التي يمسك بها RCW
  • التعبير المركّب بنقطتين أو أكثر يولّد RCW مجهولاً. استلم الكائن الوسيط في متغيّر
  • اجعل التحرير أساساً «عزل التابع + ثلاثية GC»، وإن لزم التحكّم بالترتيب، نظّم ReleaseComObject عبر غلاف using. لا تبعثر ReleaseComObject الخام
  • حدّد نسختك فقط عبر Application.HwndGetWindowThreadProcessId من أجل Kill الضماني، وفعّله فقط بترتيب «Quit ← الانتظار ← عند انتهاء المهلة فقط»
  • أتمتة Excel على الخادم أو بشكل غير مأهول تكوين غير مدعوم. انقل توليد التقارير غير المأهول إلى ClosedXML / Open XML SDK، واعزل المعالجة التي تحتاج ماكرو أو إعادة حساب في COM ضمن بيئة تفاعلية

إن وجدت صفّاً من EXCEL.EXE في مدير المهام، فهذا إشارة إما على مشكلة تقنية (الفصلان 4-5) أو مشكلة تكوين (الفصلان 6-7). إن تردّدت في تحديد أيّهما ينطبق على الشيفرة بين يديك، أو من أي نطاق تبدأ الاستبدال إن قرّرته، يمكننا المساعدة بدءاً من جرد المعالجة.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع التحقيق في مشاكل تطبيقات Windows بما فيها أتمتة Excel/Office (بقاء العملية، والتجمّد، وقفل الملفات)، وصيانة أصول COM وتنظيمها، وتصميم ترحيل معالجة التقارير إلى مكتبات منظومة Open XML.

روابط مرجعية

  1. Microsoft Learn, Runtime Callable Wrapper. حول إنشاء RCW واحد لكل كائن COM داخل العملية، وتخزين مؤشّر الواجهة مؤقّتاً، وتحرير مرجع كائن COM عند جمعه بواسطة GC.  2 3 4 5

  2. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. حول سلوك إنقاص عدّاد مراجع RCW، وخطر InvalidComObjectException وانتهاك الوصول وتلف الذاكرة عند استخدام RCW محرَّر بالفعل، والتصنيف كـ«يُستخدم فقط عند الضرورة المطلقة»، والعلاقة مع FinalReleaseComObject.  2 3 4 5 6 7

  3. Microsoft Learn, Application.hWnd property (Excel). حول أن خاصية Hwnd لكائن Application في Excel تعيد مقبض النافذة العلوية.  2 3

  4. Microsoft Learn, GetWindowThreadProcessId function (winuser.h). حول إمكانية الحصول على معرّف الخيط ومعرّف العملية اللذين أنشآ النافذة المحدَّدة.  2

  5. Microsoft Support, Considerations for server-side Automation of Office. حول عدم توصية Microsoft ودعمها لأتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعلية (بما فيها ASP، وASP.NET، وDCOM، وخدمات NT).  2 3

  6. Microsoft Learn, Welcome to the Open XML SDK for Office. حول كون Open XML SDK مكتبة مبنية على System.IO.Packaging تعالج تنسيق ملفات Office المعياري ECMA-376 / ISO/IEC 29500 عبر أصناف منمَّطة بقوة.  2

  7. GitHub, ClosedXML/ClosedXML. حول كونها مكتبة برخصة MIT توفّر واجهة حدسية فوق Open XML API، وتستطيع التعامل مع ملفات Excel 2007+ (.xlsx, .xlsm) دون تثبيت Excel.  2 3

  8. Microsoft Support, Office application does not exit after automation from Visual Studio .NET client. حول أن سبب عدم إنهاء تطبيق Office رغم استدعاء Quit هو المراجع التي يمسك بها RCW، وأن العلاج يشمل إعلان كل كائن كمتغيّر جديد (مثال استلام oExcel.Workbooks في متغيّر وسيط)، واستدعاء Marshal.ReleaseComObject حتى يعود 0، وضبط المتغيّرات إلى null، واستدعاء Quit، واستدعاء GC.Collect وGC.WaitForPendingFinalizers إن لم ينتهِ رغم ذلك.  2

  9. Microsoft Learn, _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). حول وضع أمان الماكرو عند فتح ملف برمجياً، وكون الافتراضي عند تشغيل التطبيق هو msoAutomationSecurityLow (تفعيل كل الماكرو)، وإمكانية تعطيل كل الماكرو دون تحذير عبر msoAutomationSecurityForceDisable. 

  10. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. حول مشاكل الأتمتة غير المأهولة كواجهة المستخدم التفاعلية، وتحديد المستخدم، وبنية الخيط الواحد STA، وكون السلوك AS IS حتى تحت الترخيص غير المأهول، وأن البديل الموصى به هو Microsoft Graph وتحرير تنسيق ملف Open XML مباشرة.  2 3 4 5

  11. Microsoft Learn, Open XML SDK for Office design considerations. حول أن Open XML SDK ليس بديلاً لنموذج كائنات Office، ولا يوفّر سلوكيات التطبيق كإعادة حساب الصيغ وتحديث البيانات، ولا وظيفة التحويل إلى تنسيقات أخرى.  2 3

  12. Microsoft Learn, Native interoperability ABI support. حول اقتصار دعم نظام التشغيل التبادلي مع COM المدمج على Windows، ودعم COM عبر ComWrappers في .NET 5+ والتوليد المصدري في .NET 8+. 

  13. Microsoft Learn, How to access Office interop objects. حول تبسيط التشغيل التبادلي مع Office عبر المعطيات المسمّاة والاختيارية وdynamic، وكون تضمين الأنواع التبادلية (Embed Interop Types) هو السلوك الافتراضي بدلاً من PIA.  2

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

لماذا لا تنتهي عملية EXCEL.EXE رغم استدعاء Quit()؟
لأن Quit() ليس تحريراً، بل مجرّد طلب بمعنى «يجوز الإنهاء عندما تُحرَّر جميع المراجع». يتعامل .NET مع كائنات COM عبر غلاف يُسمّى RCW (Runtime Callable Wrapper)، ويستمر RCW في الإمساك بمرجع COM حتى يُجمع بواسطة GC (أو يُحرَّر صراحة). وطالما بقي مرجع ما، ينتظر Excel بأمانة. وبما أن توقيت التحرير يعتمد على GC وغير محدَّد، فإن عدم قابلية إعادة الإنتاج من نوع «يختفي في جهاز التطوير لكنه يبقى في الإنتاج» يمكن تفسيرها أيضاً بتذبذب توقيت GC.
ما هي قاعدة النقطتين في تشغيل Excel عبر COM؟
قاعدة تجريبية تنص على عدم ربط نقطتين أو أكثر متتاليتين على كائن COM، واستلام كل كائن وسيط في متغيّر مرّة واحدة. فتعبير مركّب مثل book.Worksheets[1].Range["A1"] ينشئ عدّة RCW مجهولة في سطر واحد: Worksheets (مجموعة)، وWorksheets[1] (ورقة)، وRange["A1"] (نطاق). وبما أن هذه لا تُوضع في متغيّرات، لا توجد وسيلة لاستدعاء Marshal.ReleaseComObject عليها، فتصبح الفاعل الرئيسي لعدم التحرير. يجب الانتباه إلى أن القراءة والتجاهل داخل foreach أو تعبير شرطي، أو معطيات تعبير مركّب، تولّد أيضاً RCW مجهولة بالطريقة نفسها.
كيف تُكتب الشيفرة لإنهاء EXCEL.EXE بشكل مضمون؟
الموصى به هو عزل المعالجة التي تلمس Excel بالكامل في تابع واحد، وتنفيذ ثلاثية GC.Collect ← GC.WaitForPendingFinalizers ← GC.Collect بعد الخروج من ذلك التابع. بما أن RCW مصمَّم أصلاً على تحرير مرجع COM عند جمعه بواسطة GC، فإن هذا الشكل يجمع حتى الـ RCW المجهولة حتى لو خُرقت قاعدة النقطتين. توجد أيضاً طريقة تطبيق Marshal.ReleaseComObject على كل الكائنات، لكن الوثائق الرسمية تصنّفها بأنها «تُستخدم فقط عند الضرورة المطلقة»، وسوء استخدام RCW المحرَّر بالفعل يؤدّي إلى InvalidComObjectException أو تلف الذاكرة. أدخل غلافاً منظّماً عبر using فقط عند الحاجة إلى التحكّم بترتيب التحرير.
هل يجوز أتمتة Excel من خادم أو دفعة (batch)؟
تنص Microsoft صراحة على أنها لا توصي، ولا تدعم، أتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعلية (بما فيها ASP.NET، وDCOM، وخدمات NT). صُمّم Office على افتراض وجود مستخدم تفاعلي، ولذا يحدث على الخدمات أو IIS تجمّد بانتظار مربّع حوار أو تراكم لا نهائي للعمليات. لتوليد تقارير غير مأهول، البديل الموصى به هو الاستبدال بـ Open XML SDK أو ClosedXML اللتين تعالجان الملف مباشرة دون تشغيل Excel. التقسيم الواقعي هو عزل المعالجة التي تحتاج فعلياً إلى ميزات Excel نفسه (تنفيذ ماكرو، إعادة حساب) فقط في COM ضمن بيئة تفاعلية.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة