سؤال

أقوم بكتابة برنامج كالتالي:

  • ابحث عن كافة الملفات ذات الامتداد الصحيح في دليل معين
  • Foreach، ابحث عن كافة تكرارات سلسلة معينة في تلك الملفات
  • طباعة كل سطر

أود أن أكتب هذا بطريقة وظيفية، كسلسلة من وظائف المولد (الأشياء التي تستدعي yield return وإرجاع عنصر واحد فقط في كل مرة يتم تحميلها بتكاسل)، لذلك سيكون الكود الخاص بي كما يلي:

IEnumerable<string> allFiles = GetAllFiles();
IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles );
IEnumerable<string> contents = GetFileContents( matchingFiles );
IEnumerable<string> matchingLines = GetMatchingLines( contents );

foreach( var lineText in matchingLines )
  Console.WriteLine( "Found: " + lineText );

كل هذا جيد، لكن ما أود فعله أيضًا هو طباعة بعض الإحصائيات في النهاية.شيء من هذا القبيل:

Found 233 matches in 150 matching files. Scanned 3,297 total files in 5.72s

تكمن المشكلة في كتابة التعليمات البرمجية بأسلوب "وظيفي خالص" كما هو مذكور أعلاه، حيث يتم تحميل كل عنصر ببطء.
أنت تعرف فقط عدد الملفات المتطابقة إجمالاً حتى تكتمل حلقة foreach النهائية، ولأن هناك عنصرًا واحدًا فقط موجودًا على الإطلاق yieldعند تحريره في كل مرة، لا يكون للكود أي مكان لتتبع عدد الأشياء التي تم العثور عليها مسبقًا.إذا قمت باستدعاء LINQ matchingLines.Count() الطريقة، فإنه سيتم إعادة تعداد المجموعة!

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

أيه أفكار؟هتافات

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

المحلول

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

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

وهنا أ Counter فئة قمت بتجميعها للتو - ولكن يمكنك إنشاء أشكال مختلفة لأنواع التجميع الأخرى أيضًا.

public class Counter<T> : IEnumerable<T>
{
    public int Count { get; private set; }

    public Counter(IEnumerable<T> source)
    {
        mSource = source;
        Count = 0;
    }

    public IEnumerator<T> GetEnumerator()
    {
        foreach (var T in mSource)
        {
            Count++;
            yield return T;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        foreach (var T in mSource)
        {
            Count++;
            yield return T;
        }
    }

    private IEnumerable<T> mSource;
}

يمكنك إنشاء ثلاث حالات من Counter:

  1. واحدة للالتفاف GetAllFiles() حساب العدد الإجمالي للملفات.
  2. واحدة للالتفاف GetMatches() حساب عدد الملفات المطابقة؛و
  3. واحدة للالتفاف GetMatchingLines() حساب عدد الأسطر المطابقة.

المفتاح في هذا النهج هو أنك لا تضع مسؤوليات متعددة على فئاتك/أساليبك الحالية - GetMatchingLines() تتعامل الطريقة مع المطابقة فقط، فأنت لا تطلب منها تتبع الإحصائيات أيضًا.

إيضاح ردا على تعليق من Mitcham:

سيبدو الكود النهائي كالتالي:

var files = new Counter<string>( GetAllFiles());
var matchingFiles = new Counter<string>(GetMatches( "*.txt", files ));
var contents = GetFileContents( matchingFiles );
var linesFound = new Counter<string>(GetMatchingLines( contents ));

foreach( var lineText in linesFound )
    Console.WriteLine( "Found: " + lineText );

string message 
    = String.Format( 
        "Found {0} matches in {1} matching files. Scanned {2} files",
        linesFound.Count,
        matchingFiles.Count,
        files.Count);
Console.WriteLine(message);

لاحظ أن هذا لا يزال نهجًا وظيفيًا - المتغيرات المستخدمة هي غير قابل للتغيير (أشبه الارتباطات من المتغيرات)، والوظيفة الشاملة ليس لها أي آثار جانبية.

نصائح أخرى

أود أن أقول إنك بحاجة إلى تغليف العملية في فئة "المطابقة" التي تلتقط فيها أساليبك الإحصائيات أثناء تقدمها.

public class Matcher
{
  private int totalFileCount;
  private int matchedCount;
  private DateTime start;
  private int lineCount;
  private DateTime stop;

  public IEnumerable<string> Match()
  {
     return GetMatchedFiles();
     System.Console.WriteLine(string.Format(
       "Found {0} matches in {1} matching files." + 
       " {2} total files scanned in {3}.", 
       lineCount, matchedCount, 
       totalFileCount, (stop-start).ToString());
  }

  private IEnumerable<File> GetMatchedFiles(string pattern)
  {
     foreach(File file in SomeFileRetrievalMethod())
     {
        totalFileCount++;
        if (MatchPattern(pattern,file.FileName))
        {
          matchedCount++;
          yield return file;
        }
     }
  }
}

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

يمكنني التفكير في فكرتين

  1. قم بتمرير كائن سياق وإرجاع (سلسلة + سياق) من العدادين لديك - الحل الوظيفي البحت

  2. استخدم التخزين المحلي لمؤشر الترابط لإحصائياتك (CallContext)، يمكنك أن تكون خياليًا وتدعم مجموعة من السياقات.لذلك سيكون لديك رمز مثل هذا.

    using (var stats = DirStats.Create())
    {
        IEnumerable<string> allFiles = GetAllFiles();
        IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles );
        IEnumerable<string> contents = GetFileContents( matchingFiles );
        stats.Print()
        IEnumerable<string> matchingLines = GetMatchingLines( contents );
        stats.Print();
    } 
    

إذا كنت سعيدًا بقلب التعليمات البرمجية الخاصة بك رأسًا على عقب، فقد تكون مهتمًا بـ Push LINQ.الفكرة الأساسية هي عكس نموذج "السحب". IEnumerable<T> وتحويله إلى نموذج "دفع" مع المراقبين - يقوم كل جزء من المسار بدفع بياناته بشكل فعال إلى ما بعد أي عدد من المراقبين (باستخدام معالجات الأحداث) التي تشكل عادةً أجزاء جديدة من المسار.وهذا يوفر طريقة سهلة حقًا لربط مجموعات متعددة بنفس البيانات.

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

إنه مشروع صغير ممتع، لكنه يتطلب القليل من التفكير.

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

public class Counter
{
    public int Count { get; set; }
}

public static class CounterExtensions
{
    public static IEnumerable<T> ObserveCount<T>
      (this IEnumerable<T> source, Counter count)
    {
        foreach (T t in source)
        {
            count.Count++;
            yield return t;
        }
    }

    public static IEnumerable<T> ObserveCount<T>
      (this IEnumerable<T> source, IList<Counter> counters)
    {
        Counter c = new Counter();
        counters.Add(c);
        return source.ObserveCount(c);
    }
}


public static class CounterTest
{
    public static void Test1()
    {
        IList<Counter> counters = new List<Counter>();
  //
        IEnumerable<int> step1 =
            Enumerable.Range(0, 100).ObserveCount(counters);
  //
        IEnumerable<int> step2 =
            step1.Where(i => i % 10 == 0).ObserveCount(counters);
  //
        IEnumerable<int> step3 =
            step2.Take(3).ObserveCount(counters);
  //
        step3.ToList();
        foreach (Counter c in counters)
        {
            Console.WriteLine(c.Count);
        }
    }
}

الإخراج كما هو متوقع:21، 3، 3

بافتراض أن هذه الوظائف خاصة بك، فإن الشيء الوحيد الذي يمكنني التفكير فيه هو نمط الزائر، حيث يمرر وظيفة زائر مجردة تستدعيك مرة أخرى عند حدوث كل شيء.على سبيل المثال:قم بتمرير ILineVisitor إلى GetFileContents (والذي أفترض أنه يقسم الملف إلى أسطر).سيكون لدى ILineVisitor طريقة مثل OnVisitLine (خط السلسلة)، ويمكنك بعد ذلك تنفيذ ILineVisitor وجعله يحتفظ بالإحصائيات المناسبة.اشطفه وكرر ذلك باستخدام IlineMatchVisitor وIFileVisitor وما إلى ذلك.أو يمكنك استخدام IVisitor واحد مع طريقة OnVisit() التي لها دلالات مختلفة في كل حالة.

ستحتاج كل وظيفة من وظائفك إلى أخذ زائر، وتسميته OnVisit() في الوقت المناسب، الأمر الذي قد يبدو مزعجًا، ولكن على الأقل يمكن استخدام الزائر للقيام بالكثير من الأشياء المثيرة للاهتمام، بخلاف ما تفعله هنا فقط .في الواقع، يمكنك تجنب كتابة GetMatchingLines عن طريق تمرير زائر يتحقق من التطابق في OnVisitLine(String line) في GetFileContents.

هل هذا أحد الأشياء القبيحة التي فكرت فيها بالفعل؟

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