سؤال

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

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

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

bool isFirst = true;
foreach (var element in array)
{
  if (!isFirst)
  {
     append(delimiter);
  }
  else
  {
    isFirst = false;
  }

  append(element);
}

ملاحظة: إن التحسين الطفيف لهذا هو إزالة آخر ووضعه في نهاية الحلقة. عادة ما تكون المهمة تعليمة واحدة وتعادل آخر وتقلل من عدد الكتل الأساسية بمقدار 1 وتزيد من حجم الكتلة الأساسية للجزء الرئيسي. النتيجة هي تنفيذ شرط في كل حلقة لتحديد ما إذا كان يجب عليك إضافة المحدد أم لا.

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

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

كنت متحمسًا بعد ذلك لقراءة هذا البناء

var enumerator = array.GetEnumerator();
if (enumerator.MoveNext())
{
  goto start;
  do {
    append(delimiter);

  start:
    append(enumerator.Current);
  } while (enumerator.MoveNext());
}

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

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

ربما تكون أكثر تحديدا مع المتطلبات:

  • لا تكرر الكود
  • لا تفعل عمل غير ضروري
  • لا تكن أكثر من 2 أو 3 مرات أبطأ من رمز آخر
  • كن قابلاً للقراءة

أعتقد أن قابلية القراءة هي الشيء الوحيد الذي قد يعاني من الوصفة التي ذكرتها. ومع ذلك ، لا يعمل في C# ، فما هو أفضل شيء التالي؟

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

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

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

المحلول

لمثالك المحدد ، هناك حل قياسي: string.Join. هذا يتعامل مع إضافة المحدد بشكل صحيح بحيث لا تضطر إلى كتابة الحلقة بنفسك.

إذا كنت تريد حقًا أن تكتب هذا بنفسك ، يمكنك استخدام ما يلي:

string delimiter = "";
foreach (var element in array)
{
    append(delimiter);
    append(element);
    delimiter = ",";
}

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

نصائح أخرى

أنا شخصياً أحب خيار Mark Byer ، ولكن يمكنك دائمًا كتابة الطريقة العامة الخاصة بك لهذا:

public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source,
    Action<T> firstAction,
    Action<T> subsequentActions)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (iterator.MoveNext())
        {
            firstAction(iterator.Current);
        }
        while (iterator.MoveNext())
        {
            subsequentActions(iterator.Current);
        }
    }
}

هذا واضح نسبيًا ... إعطاء خاص الاخير العمل أصعب قليلاً:

public static void IterateWithSpecialLast<T>(this IEnumerable<T> source,
    Action<T> allButLastAction,
    Action<T> lastAction)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            return;
        }            
        T previous = iterator.Current;
        while (iterator.MoveNext())
        {
            allButLastAction(previous);
            previous = iterator.Current;
        }
        lastAction(previous);
    }
}

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

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

أنت بالفعل على استعداد للتخلي عن foreach. لذلك يجب أن يكون هذا مناسبًا:

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }

يمكنك بالتأكيد إنشاء ملف goto الحل في C# (ملاحظة: لم أضيف null الفحوصات):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  var enumerator = array.GetEnumerator();
  if (enumerator.MoveNext()) {
    goto start;
    loop:
      sb.Append(delimiter);
      start: sb.Append(enumerator.Current);
      if (enumerator.MoveNext()) goto loop;
  }
  return sb.ToString();
}

من اجلك محدد على سبيل المثال ، يبدو هذا أمرًا شديدًا بالنسبة لي (وهو أحد الحلول التي وصفتها):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  foreach (string element in array) {
    sb.Append(element);
    sb.Append(delimiter);
  }
  if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length;
  return sb.ToString();
}

إذا كنت ترغب في الحصول على وظيفية ، يمكنك محاولة استخدام هذا النهج القابل للطي:

string Join(string[] array, string delimiter) {
  return array.Aggregate((left, right) => left + delimiter + right);
}

على الرغم من أنه يقرأ لطيفًا حقًا ، إلا أنه لا يستخدم StringBuilder, ، لذلك قد ترغب في الإساءة Aggregate قليلا لاستخدامه:

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  array.Aggregate((left, right) => {
    sb.Append(left).Append(delimiter).Append(right);
    return "";
  });
  return sb.ToString();
}

أو يمكنك استخدام هذا (استعارة الفكرة من إجابات أخرى هنا):

string Join(string[] array, string delimiter) {
  return array.
    Skip(1).
    Aggregate(new StringBuilder(array.FirstOrDefault()),
      (acc, s) => acc.Append(delimiter).Append(s)).
    ToString();
}

أحيانًا أستخدم LINQ .First() و .Skip(1) للتعامل مع هذا ... يمكن أن يعطي هذا حلًا نظيفًا نسبيًا (وقابل للقراءة للغاية).

استخدامك مثال ،

append(array.First());
foreach(var x in array.Skip(1))
{
  append(delimiter);
  append (x);
}

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

استخدم F# سيكون اقتراحًا آخر :-)

هناك طرق "يمكنك" الالتفاف على الكود المضاعف ، ولكن في معظم الحالات يكون الكود المكرر أقل قبيحة/خطرة من الحلول الممكنة. لا يبدو حل "goto" الذي تقتبسه بمثابة تحسن بالنسبة لي - لا أعتقد حقًا أنك تكتسب حقًا أي شيء مهم (الانضغاط أو قابلية القراءة أو كفاءته) باستخدامه ، بينما تزيد من خطر حصول المبرمج على خطأ ما في مرحلة ما من عمر الكود.

بشكل عام ، أميل إلى الذهاب للنهج:

  • حالة خاصة للإجراء الأول (أو الأخير)
  • حلقة للأفعال الأخرى.

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

أو نهج آخر أستخدمه أحيانًا حيث تكون الكفاءة مهمة:

  • حلقة ، واختبار ما إذا كانت السلسلة فارغة لتحديد ما إذا كان المحدد مطلوبًا.

يمكن كتابة هذا ليكون أكثر إحكاما وقابلة للقراءة من نهج GOTO ، ولا يتطلب أي متغيرات/تخزين/اختبارات إضافية للكشف عن "الحالة الخاصة" eTeraiton.

لكنني أعتقد أن مقاربة مارك بايرز هي حل نظيف جيد لمثالك الخاص.

أنا أفضل first طريقة متغيرة. ربما ليست أنظف ولكن أكثر الطرق كفاءة. بدلاً من ذلك يمكنك استخدامه Length من الشيء الذي ترفقه ومقارنته بالصفر. يعمل بشكل جيد مع StringBuilder.

لماذا لا تتحرك التعامل مع العنصر الأول خارج حلقة؟

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}

إذا كنت ترغب في السير في المسار الوظيفي ، فيمكنك تحديد String.Soin مثل إنشاء LINQ الذي يمكن إعادة استخدامه عبر الأنواع.

شخصياً ، سأذهب دائمًا إلى وضوح الرمز حول توفير عدد قليل من عمليات الإعدام.

على سبيل المثال:

namespace Play
{
    public static class LinqExtensions {
        public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner)
        {
            U joined = default(U);
            bool first = true;
            foreach (var item in list)
            {
                if (first)
                {
                    joined = initializer(item);
                    first = false;
                }
                else
                {
                    joined = joiner(joined, item);
                }
            }
            return joined;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            List<int> nums = new List<int>() { 1, 2, 3 };
            var sum = nums.JoinElements(a => a, (a, b) => a + b);
            Console.WriteLine(sum); // outputs 6

            List<string> words = new List<string>() { "a", "b", "c" };
            var buffer = words.JoinElements(
                a => new StringBuilder(a), 
                (a, b) => a.Append(",").Append(b)
                );

            Console.WriteLine(buffer); // outputs "a,b,c"

            Console.ReadKey();
        }

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