سؤال

عند كتابة عبارة التبديل، يبدو أن هناك قيدين على ما يمكنك تشغيله في عبارات الحالة.

على سبيل المثال (ونعم، أعلم، إذا كنت تفعل هذا النوع من الأشياء، فمن المحتمل أن يعني ذلك أنك وجوه المنحى (OO) الهندسة المعمارية غير واضحة - وهذا مجرد مثال مفتعل!)

  Type t = typeof(int);

  switch (t) {

    case typeof(int):
      Console.WriteLine("int!");
      break;

    case typeof(string):
      Console.WriteLine("string!");
      break;

    default:
      Console.WriteLine("unknown!");
      break;
  }

هنا تفشل عبارة Switch() مع "قيمة من نوع متكامل متوقعة" وتفشل عبارات الحالة مع "من المتوقع وجود قيمة ثابتة".

لماذا تم فرض هذه القيود، وما هو مبررها؟لا أرى أي سبب لبيان التبديل لديه للخضوع للتحليل الثابت فقط، ولماذا يجب أن تكون القيمة التي يتم تشغيلها متكاملة (أي بدائية).ما هو المبرر؟

هل كانت مفيدة؟

المحلول

هذه هي مشاركتي الأصلية، والتي أثارت بعض الجدل.. لأنه خطأ:

بيان التبديل ليس هو نفس الشيء كبيان كبير if-else.يجب أن تكون كل حالة فريدة من نوعها وتقييمها بشكل ثابت.يقوم بيان التبديل بفرع الوقت الثابت بغض النظر عن عدد الحالات التي لديك.يقوم البيان IF-ELSE بتقييم كل حالة حتى يجد واحدة صحيحة.


في الواقع، بيان التبديل C# هو لا دائما فرع وقت ثابت.

في بعض الحالات، سيستخدم المترجم بيان تبديل CIL وهو في الواقع فرع زمني ثابت باستخدام جدول الانتقال.ومع ذلك، في حالات متفرقة كما أشار إيفان هاميلتون قد يقوم المترجم بإنشاء شيء آخر تمامًا.

من السهل جدًا التحقق من ذلك من خلال كتابة عبارات تبديل C# المختلفة، بعضها متفرق وبعضها كثيف، والنظر إلى CIL الناتج باستخدام أداة ildasm.exe.

نصائح أخرى

من المهم عدم الخلط بين بيان التبديل C# وتعليمات التبديل CIL.

يعد مفتاح CIL عبارة عن جدول انتقال يتطلب فهرسًا لمجموعة من عناوين الانتقال.

يكون هذا مفيدًا فقط إذا كانت حالات مفتاح C# متجاورة:

case 3: blah; break;
case 4: blah; break;
case 5: blah; break;

لكن لا فائدة منها إذا لم تكن كذلك:

case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;

(ستحتاج إلى جدول بحجم 3000 إدخال تقريبًا، مع استخدام 3 فتحات فقط)

مع التعبيرات غير المتجاورة، قد يبدأ المترجم في إجراء عمليات التحقق الخطية من if-else-if-else.

مع مجموعات التعبير الأكبر غير المتجاورة، قد يبدأ المترجم ببحث شجرة ثنائية، وأخيرًا العناصر القليلة الأخيرة.

مع مجموعات التعبير التي تحتوي على مجموعات من العناصر المتجاورة، قد يقوم المترجم بالبحث عن شجرة ثنائية، وأخيرًا مفتاح CIL.

هذا مليء بـ "mays" و"mights"، ويعتمد على المترجم (قد يختلف مع Mono أو Rotor).

لقد قمت بتكرار نتائجك على جهازي باستخدام الحالات المجاورة:

إجمالي الوقت لتنفيذ مفتاح 10 اتجاهات، 10000 تكرار (مللي ثانية):25.1383
الوقت التقريبي لكل مفتاح 10 اتجاهات (مللي ثانية):0.00251383

إجمالي الوقت لتنفيذ مفتاح 50 اتجاهًا، 10000 تكرار (مللي ثانية):26.593
الوقت التقريبي لكل 50 مفتاح تبديل (مللي ثانية):0.0026593

الوقت الإجمالي لتنفيذ 5000 طريقة تبديل، 10000 تكرار (مللي ثانية):23.7094
الوقت التقريبي لكل 5000 مفتاح تبديل (مللي ثانية):0.00237094

إجمالي الوقت لتنفيذ 50000 طريقة تبديل، 10000 تكرار (مللي ثانية):20.0933
الوقت التقريبي لكل 50000 مفتاح تبديل (مللي ثانية):0.00200933

ثم قمت أيضًا باستخدام تعبيرات الحالة غير المتجاورة:

إجمالي الوقت لتنفيذ مفتاح 10 اتجاهات، 10000 تكرار (مللي ثانية):19.6189
الوقت التقريبي لكل مفتاح 10 اتجاهات (مللي ثانية):0.00196189

إجمالي الوقت لتنفيذ مفتاح 500 اتجاه، 10000 تكرار (مللي ثانية):19.1664
الوقت التقريبي لكل 500 مفتاح تبديل (مللي ثانية):0.00191664

الوقت الإجمالي لتنفيذ 5000 طريقة تبديل، 10000 تكرار (مللي ثانية):19.5871
الوقت التقريبي لكل 5000 مفتاح تبديل (مللي ثانية):0.00195871

لن يتم تجميع عبارة تبديل الحالة غير المتجاورة التي يبلغ عددها 50000.
"التعبير طويل جدًا أو معقد بحيث لا يمكن تجميعه بالقرب من 'ConsoleApplication1.Program.Main(string[])'

الأمر المضحك هنا هو أن بحث الشجرة الثنائية يبدو أسرع قليلاً (ربما ليس من الناحية الإحصائية) من تعليمات تبديل CIL.

بريان، لقد استخدمت كلمة "ثابت"، والتي لها معنى محدد للغاية من منظور نظرية التعقيد الحسابي.في حين أن مثال الأعداد الصحيحة المتجاورة المبسطة قد ينتج CIL الذي يعتبر O(1) (ثابت)، فإن المثال المتناثر هو O(log n) (لوغاريتمي)، وتقع الأمثلة المجمعة في مكان ما بينهما، والأمثلة الصغيرة هي O(n) (خطي ).

هذا لا يعالج حتى حالة السلسلة، حيث يتم استخدام static Generic.Dictionary<string,int32> قد يتم إنشاؤها، وسوف تعاني من الحمل واضح عند الاستخدام الأول.الأداء هنا سوف يعتمد على أداء Generic.Dictionary.

إذا قمت بالتحقق من مواصفات لغة C# (ليس مواصفات CIL) ستجد "15.7.2 عبارة التبديل" لا تذكر "وقت ثابت" أو أن التنفيذ الأساسي يستخدم حتى تعليمات مفتاح CIL (كن حذرًا جدًا في افتراض مثل هذه الأشياء).

في نهاية المطاف، يعد تبديل لغة C# مقابل تعبير عدد صحيح في نظام حديث بمثابة عملية أقل من ميكروثانية، ولا تستحق القلق بشأنها عادةً.


بالطبع ستعتمد هذه الأوقات على الآلات والظروف.لن أهتم باختبارات التوقيت هذه، ففترات الميكروثانية التي نتحدث عنها تتضاءل أمام أي كود "حقيقي" يتم تشغيله (ويجب عليك تضمين بعض "الكود الحقيقي" وإلا فسيقوم المترجم بتحسين الفرع بعيدًا)، أو غضب في النظام.تعتمد إجاباتي على استخدام إل ديسم لفحص CIL الذي أنشأه مترجم C#.بالطبع، هذا ليس نهائيًا، حيث يتم إنشاء التعليمات الفعلية التي تقوم بتشغيلها وحدة المعالجة المركزية بواسطة JIT.

لقد قمت بمراجعة تعليمات وحدة المعالجة المركزية النهائية التي تم تنفيذها فعليًا على جهاز x86 الخاص بي، ويمكنني التأكد من أن مفتاح المجموعة المجاور البسيط يقوم بشيء مثل:

  jmp     ds:300025F0[eax*4]

حيث يكون بحث الشجرة الثنائية مليئًا بما يلي:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  …
  cmp     ebx, 0F82h
  jz      30005EEE

السبب الأول الذي يتبادر إلى الذهن هو تاريخي:

وبما أن معظم مبرمجي C وC++ وJava ليسوا معتادين على التمتع بمثل هذه الحريات، فإنهم لا يطالبون بها.

سبب آخر أكثر صحة هو أن سوف يزيد تعقيد اللغة:

بادئ ذي بدء، ينبغي مقارنة الكائنات مع .Equals() أو مع == المشغل أو العامل؟وكلاهما صالح في بعض الحالات.هل يجب علينا تقديم بناء جملة جديد للقيام بذلك؟هل يجب أن نسمح للمبرمج بتقديم طريقة المقارنة الخاصة به؟

بالإضافة إلى ذلك، فإن السماح بتشغيل الكائنات من شأنه أن يؤدي إلى حدوث ذلك كسر الافتراضات الأساسية حول بيان التبديل.هناك قاعدتان تحكمان عبارة التبديل والتي لن يتمكن المترجم من فرضها إذا تم السماح بتشغيل الكائنات (راجع مواصفات لغة الإصدار C# 3.0, §8.7.2):

  • أن قيم تسميات التبديل هي ثابت
  • أن قيم تسميات التبديل هي متميز (بحيث يمكن تحديد كتلة تبديل واحدة فقط لتعبير تبديل معين)

خذ بعين الاعتبار مثال التعليمات البرمجية هذا في الحالة الافتراضية التي يسمح فيها بقيم الحالة غير الثابتة:

void DoIt()
{
    String foo = "bar";
    Switch(foo, foo);
}

void Switch(String val1, String val2)
{
    switch ("bar")
    {
        // The compiler will not know that val1 and val2 are not distinct
        case val1:
            // Is this case block selected?
            break;
        case val2:
            // Or this one?
            break;
        case "bar":
            // Or perhaps this one?
            break;
    }
}

ماذا سيفعل الكود؟ماذا لو تم إعادة ترتيب بيانات الحالة؟في الواقع، أحد الأسباب التي جعلت C# تجعل التبديل غير قانوني هو أنه يمكن إعادة ترتيب عبارات التبديل بشكل تعسفي.

تم وضع هذه القواعد لسبب ما - بحيث يستطيع المبرمج، من خلال النظر إلى كتلة حالة واحدة، أن يعرف على وجه اليقين الحالة الدقيقة التي يتم بموجبها إدخال الكتلة.عندما ينمو بيان التبديل المذكور أعلاه إلى 100 سطر أو أكثر (وسيفعل ذلك)، فإن هذه المعرفة لا تقدر بثمن.

بالمناسبة، VB، الذي يتمتع بنفس البنية الأساسية، يسمح بمرونة أكبر Select Case العبارات (الكود أعلاه سيعمل في VB) ولا يزال ينتج تعليمات برمجية فعالة حيث يكون ذلك ممكنًا، لذا يجب النظر بعناية في الحجة المتعلقة بالقيود التقنية.

في الغالب، هذه القيود موجودة بسبب مصممي اللغة.قد يكون المبرر الأساسي هو التوافق مع تاريخ اللغة أو المثل العليا أو تبسيط تصميم المترجم.

قد يختار المترجم (ويفعل) ما يلي:

  • إنشاء عبارة if-else كبيرة
  • استخدم تعليمات تبديل MSIL (جدول القفز)
  • بناء عامu003Cstring,int32> ، ملءه عند الاستخدام الأول ، واتصل generic.dictionary <> :: trygetValue () لفهرس لتمريره إلى تعليمات مفتاح MSIL (جدول القفز)
  • استخدم مجموعة من القفزات if-elses & msil "التبديل"

بيان التبديل ليس فرعًا زمنيًا ثابتًا.قد يجد المترجم طرقًا مختصرة (باستخدام دلاء التجزئة، وما إلى ذلك)، ولكن الحالات الأكثر تعقيدًا ستولد كود MSIL أكثر تعقيدًا مع تفرع بعض الحالات قبل غيرها.

للتعامل مع حالة السلسلة، سينتهي المترجم (في مرحلة ما) باستخدام a.Equals(b) (وربما a.GetHashCode() ).أعتقد أنه سيكون من السهل على المترجم استخدام أي كائن يلبي هذه القيود.

أما بالنسبة للحاجة إلى تعبيرات الحالة الثابتة ...لن تكون بعض هذه التحسينات (التجزئة، والتخزين المؤقت، وما إلى ذلك) متاحة إذا لم تكن تعبيرات الحالة حتمية.لكننا رأينا بالفعل أنه في بعض الأحيان يختار المترجم الطريق التبسيطي if-else-if-else على أية حال...

يحرر: com.lomaxx - فهمك لعامل التشغيل "typeof" غير صحيح.يتم استخدام عامل التشغيل "typeof" للحصول على كائن System.Type لنوع ما (لا علاقة له بالأنواع الفائقة أو الواجهات).إن التحقق من توافق وقت التشغيل لكائن مع نوع معين هو مهمة المشغل "is".استخدام "typeof" هنا للتعبير عن كائن غير ذي صلة.

أثناء تناول هذا الموضوع، وفقًا لجيف أتوود، بيان التبديل هو فظاعة البرمجة.استخدمها باعتدال.

يمكنك غالبًا إنجاز نفس المهمة باستخدام جدول.على سبيل المثال:

var table = new Dictionary<Type, string>()
{
   { typeof(int), "it's an int!" }
   { typeof(string), "it's a string!" }
};

Type someType = typeof(int);
Console.WriteLine(table[someType]);

لا أرى أي سبب يجعل بيان التبديل يخضع للتحليل الثابت فقط

صحيح، لا يملك إلى، والعديد من اللغات تستخدم في الواقع عبارات التبديل الديناميكية.هذا يعني أن إعادة ترتيب عبارات "الحالة" يمكن أن تغير سلوك الكود.

هناك بعض المعلومات المثيرة للاهتمام وراء قرارات التصميم التي تم إدخالها في "التبديل" هنا: لماذا تم تصميم بيان التبديل C# بحيث لا يسمح بالاختراق، ولكنه لا يزال يتطلب استراحة؟

يمكن أن يؤدي السماح بتعبيرات الحالة الديناميكية إلى حدوث أشياء وحشية مثل كود PHP هذا:

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

والتي يجب بصراحة أن تستخدم فقط if-else إفادة.

سمعتك مايكروسوفت أخيرًا!

الآن مع C#7 يمكنك:

switch(shape)
{
case Circle c:
    WriteLine($"circle with radius {c.Radius}");
    break;
case Rectangle s when (s.Length == s.Height):
    WriteLine($"{s.Length} x {s.Height} square");
    break;
case Rectangle r:
    WriteLine($"{r.Length} x {r.Height} rectangle");
    break;
default:
    WriteLine("<unknown shape>");
    break;
case null:
    throw new ArgumentNullException(nameof(shape));
}

هذا ليس سببًا لذلك، لكن قسم مواصفات C# 8.7.2 ينص على ما يلي:

يتم تحديد نوع التحكم في بيان التبديل بواسطة تعبير التبديل.إذا كان نوع تعبير التبديل هو sbyte، أو byte، أو short، أو ushort، أو int، أو uint، أو long، أو ulong، أو char، أو string، أو نوع التعداد، فهذا هو النوع الحاكم لبيان التبديل.بخلاف ذلك، يجب أن يوجد تحويل ضمني واحد محدد من قبل المستخدم (الفقرة 6.4) من نوع تعبير التبديل إلى أحد أنواع التحكم المحتملة التالية:sbyte، byte، short، ushort، int، uint، long، ulong، char، string.في حالة عدم وجود مثل هذا التحويل الضمني، أو في حالة وجود أكثر من تحويل ضمني، يحدث خطأ في وقت الترجمة.

مواصفات C#3.0 موجودة في:http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc

أعطتني إجابة يهوذا أعلاه فكرة.يمكنك "تزييف" سلوك تبديل OP أعلاه باستخدام ملف Dictionary<Type, Func<T>:

Dictionary<Type, Func<object, string,  string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
                    {
                        return string.Format("{0}: {1}", s, o.ToString());
                    });

يتيح لك هذا ربط السلوك بنوع بنفس نمط عبارة التبديل.أعتقد أن لها فائدة إضافية تتمثل في كونها مفتاحًا بدلاً من جدول الانتقال بنمط التبديل عند تجميعها إلى IL.

أفترض أنه لا يوجد سبب أساسي لعدم تمكن المترجم من ترجمة بيان التبديل الخاص بك تلقائيًا إلى:

if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...

ولكن ليس هناك الكثير المكتسب من ذلك.

يسمح بيان الحالة على الأنواع المتكاملة للمترجم بإجراء عدد من التحسينات:

  1. لا يوجد أي تكرار (إلا إذا قمت بتكرار تسميات الحالة، والتي يكتشفها المترجم).في المثال الخاص بك، يمكن أن يتطابق مع أنواع متعددة بسبب الميراث.هل يجب تنفيذ المباراة الأولى؟كل منهم؟

  2. يمكن للمترجم اختيار تنفيذ بيان التبديل على نوع متكامل من خلال جدول الانتقال لتجنب كافة المقارنات.إذا كنت تقوم بتشغيل تعداد يحتوي على قيم عددية من 0 إلى 100، فإنه يقوم بإنشاء مصفوفة تحتوي على 100 مؤشر، واحد لكل عبارة تبديل.في وقت التشغيل، يقوم ببساطة بالبحث عن العنوان من المصفوفة بناءً على قيمة العدد الصحيح الذي يتم تشغيله.وهذا يجعل أداء وقت التشغيل أفضل بكثير من إجراء 100 مقارنة.

وفق وثائق بيان التبديل إذا كانت هناك طريقة لا لبس فيها لتحويل الكائن ضمنيًا إلى نوع متكامل، فسيتم السماح بذلك.أعتقد أنك تتوقع سلوكًا حيث سيتم استبدال كل بيان حالة بـ if (t == typeof(int)), ، ولكن هذا من شأنه أن يفتح علبة كاملة من الديدان عندما تضطر إلى تحميل هذا المشغل بشكل زائد.سيتغير السلوك عندما تتغير تفاصيل تنفيذ عبارة التبديل إذا كتبت تجاوز == بشكل غير صحيح.من خلال تقليل المقارنات مع الأنواع المتكاملة والسلسلة وتلك الأشياء التي يمكن اختزالها إلى أنواع متكاملة (والمقصود منها) فإنهم يتجنبون المشكلات المحتملة.

كتب:

"تُجري عبارة التبديل فرعًا زمنيًا ثابتًا بغض النظر عن عدد الحالات لديك."

وبما أن اللغة تسمح بذلك خيط النوع المراد استخدامه في بيان التبديل أفترض أن المترجم غير قادر على إنشاء تعليمات برمجية لتطبيق فرع زمني ثابت لهذا النوع ويحتاج إلى إنشاء نمط if-then.

@mweerden - آه فهمت.شكرًا.

ليس لدي الكثير من الخبرة في C# و.NET ولكن يبدو أن مصممي اللغة لا يسمحون بالوصول الثابت إلى نظام الكتابة إلا في ظروف ضيقة.ال نوع من تقوم الكلمة الأساسية بإرجاع كائن بحيث يمكن الوصول إليه في وقت التشغيل فقط.

أعتقد أن هينك نجح في تحقيق ذلك من خلال مبدأ "عدم الوصول الثابت إلى نظام الكتابة".

خيار آخر هو أنه لا يوجد ترتيب للأنواع حيث يمكن أن تكون الأرقام والسلاسل.وبالتالي، لن يتمكن مفتاح النوع من إنشاء شجرة بحث ثنائية، بل مجرد بحث خطي.

أنا أتفق مع هذا التعليق أن استخدام النهج القائم على الجدول غالبًا ما يكون أفضل.

في الإصدار C# 1.0، لم يكن هذا ممكنًا لأنه لم يكن به أسماء عامة ومندوبين مجهولين.تحتوي الإصدارات الجديدة من C# على السقالات اللازمة لإنجاز هذا العمل.من المفيد أيضًا الحصول على تدوين للقيم الحرفية للكائن.

ليس لدي أي معرفة تقريبًا بـ C#، لكنني أظن أنه تم استخدام أي من التبديلين ببساطة كما يحدث في لغات أخرى دون التفكير في جعله أكثر عمومية أو قرر المطور أن توسيعه لا يستحق ذلك.

بالمعنى الدقيق للكلمة، أنت على حق تمامًا في أنه لا يوجد سبب لوضع هذه القيود عليه.قد يشك المرء في أن السبب هو أن التنفيذ فعال للغاية في الحالات المسموح بها (كما اقترح برايان إنسينك (44921))، ولكني أشك في أن التنفيذ فعال للغاية (w.r.t.عبارات if) إذا كنت أستخدم الأعداد الصحيحة وبعض الحالات العشوائية (على سبيل المثال.345، -4574 و1234203).وعلى كل حال، ما الضرر في السماح بها في كل شيء (أو على الأقل أكثر) والقول إنها لا تصلح إلا لحالات محددة (مثل الأعداد المتتابعة (تقريبا)).

ومع ذلك، يمكنني أن أتخيل أنه قد يرغب المرء في استبعاد الأنواع لأسباب مثل تلك التي قدمها lomaxx (44918).

يحرر:@هينك (44970):إذا تمت مشاركة السلاسل إلى الحد الأقصى، فستكون السلاسل ذات المحتوى المتساوي بمثابة مؤشرات إلى نفس موقع الذاكرة أيضًا.بعد ذلك، إذا كان بإمكانك التأكد من أن السلاسل المستخدمة في الحالات يتم تخزينها على التوالي في الذاكرة، فيمكنك تنفيذ التبديل بكفاءة عالية (على سبيل المثال.مع التنفيذ بترتيب 2 مقارنة، إضافة وقفزتين).

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top