كيف يمكنني تكرار نوع الاتحاد F# التمييز في C#؟

StackOverflow https://stackoverflow.com/questions/2320919

  •  22-09-2019
  •  | 
  •  

سؤال

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

هل لديك أي أفكار؟

مثال

private abstract class QueueMessage { }

private class ClearMessage : QueueMessage 
{
    public static readonly ClearMessage Instance = new ClearMessage();

    private ClearMessage() { }
}

private class TryDequeueMessage : QueueMessage 
{
    public static readonly TryDequeueMessage Instance = new TryDequeueMessage();

    private TryDequeueMessage() { }
}

private class EnqueueMessage : QueueMessage 
{
    public TValue Item { get; private set; }

    private EnqueueMessage(TValue item)
    {
        Item = item;
    }
}

فئة الممثل

/// <summary>Represents a callback method to be executed by an Actor.</summary>
/// <typeparam name="TReply">The type of reply.</typeparam>
/// <param name="reply">The reply made by the actor.</param>
public delegate void ActorReplyCallback<TReply>(TReply reply);

/// <summary>Represents an Actor which receives and processes messages in concurrent applications.</summary>
/// <typeparam name="TMessage">The type of message this actor accepts.</typeparam>
/// <typeparam name="TReply">The type of reply made by this actor.</typeparam>
public abstract class Actor<TMessage, TReply> : IDisposable
{
    /// <summary>The default total number of threads to process messages.</summary>
    private const Int32 DefaultThreadCount = 1;


    /// <summary>Used to serialize access to the message queue.</summary>
    private readonly Locker Locker;

    /// <summary>Stores the messages until they can be processed.</summary>
    private readonly System.Collections.Generic.Queue<Message> MessageQueue;

    /// <summary>Signals the actor thread to process a new message.</summary>
    private readonly ManualResetEvent PostEvent;

    /// <summary>This tells the actor thread to stop reading from the queue.</summary>
    private readonly ManualResetEvent DisposeEvent;

    /// <summary>Processes the messages posted to the actor.</summary>
    private readonly List<Thread> ActorThreads;


    /// <summary>Initializes a new instance of the Genex.Concurrency&lt;TRequest, TResponse&gt; class.</summary>
    public Actor() : this(DefaultThreadCount) { }

    /// <summary>Initializes a new instance of the Genex.Concurrency&lt;TRequest, TResponse&gt; class.</summary>
    /// <param name="thread_count"></param>
    public Actor(Int32 thread_count)
    {
        if (thread_count < 1) throw new ArgumentOutOfRangeException("thread_count", thread_count, "Must be 1 or greater.");

        Locker = new Locker();
        MessageQueue = new System.Collections.Generic.Queue<Message>();
        EnqueueEvent = new ManualResetEvent(true);
        PostEvent = new ManualResetEvent(false);
        DisposeEvent = new ManualResetEvent(true);
        ActorThreads = new List<Thread>();

        for (Int32 i = 0; i < thread_count; i++)
        {
            var thread = new Thread(ProcessMessages);
            thread.IsBackground = true;
            thread.Start();
            ActorThreads.Add(thread);
        }
    }


    /// <summary>Posts a message and waits for the reply.</summary>
    /// <param name="value">The message to post to the actor.</param>
    /// <returns>The reply from the actor.</returns>
    public TReply PostWithReply(TMessage message)
    {
        using (var wrapper = new Message(message))
        {
            lock (Locker) MessageQueue.Enqueue(wrapper);
            PostEvent.Set();
            wrapper.Channel.CompleteEvent.WaitOne();
            return wrapper.Channel.Value;
        }
    }

    /// <summary>Posts a message to the actor and executes the callback when the reply is received.</summary>
    /// <param name="value">The message to post to the actor.</param>
    /// <param name="callback">The callback that will be invoked once the replay is received.</param>
    public void PostWithAsyncReply(TMessage value, ActorReplyCallback<TReply> callback)
    {
        if (callback == null) throw new ArgumentNullException("callback");
        ThreadPool.QueueUserWorkItem(state => callback(PostWithReply(value)));
    }

    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
    public void Dispose()
    {
        if (DisposeEvent.WaitOne(10))
        {
            DisposeEvent.Reset();
            PostEvent.Set();

            foreach (var thread in ActorThreads)
            {
                thread.Join();
            }

            ((IDisposable)PostEvent).Dispose();
            ((IDisposable)DisposeEvent).Dispose();
        }
    }

    /// <summary>Processes a message posted to the actor.</summary>
    /// <param name="message">The message to be processed.</param>
    protected abstract void ProcessMessage(Message message);

    /// <summary>Dequeues the messages passes them to ProcessMessage.</summary>
    private void ProcessMessages()
    {
        while (PostEvent.WaitOne() && DisposeEvent.WaitOne(10))
        {
            var message = (Message)null;

            while (true)
            {
                lock (Locker)
                {
                    message = MessageQueue.Count > 0 ?
                        MessageQueue.Dequeue() :
                        null;

                    if (message == null)
                    {
                        PostEvent.Reset();
                        break;
                    }
                }

                try
                {
                    ProcessMessage(message);
                }
                catch
                {

                }
            }
        }
    }


    /// <summary>Represents a message that is passed to an actor.</summary>
    protected class Message : IDisposable
    {
        /// <summary>The actual value of this message.</summary>
        public TMessage Value { get; private set; }

        /// <summary>The channel used to give a reply to this message.</summary>
        public Channel Channel { get; private set; }


        /// <summary>Initializes a new instance of Genex.Concurrency.Message class.</summary>
        /// <param name="value">The actual value of the message.</param>
        public Message(TMessage value)
        {
            Value = value;
            Channel = new Channel();
        }


        /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
        public void Dispose()
        {
            Channel.Dispose();
        }
    }

    /// <summary>Represents a channel used by an actor to reply to a message.</summary>         
    protected class Channel : IDisposable
    {
        /// <summary>The value of the reply.</summary>
        public TReply Value { get; private set; }

        /// <summary>Signifies that the message has been replied to.</summary>
        public ManualResetEvent CompleteEvent { get; private set; }


        /// <summary>Initializes a new instance of Genex.Concurrency.Channel class.</summary>
        public Channel()
        {
            CompleteEvent = new ManualResetEvent(false);
        }

        /// <summary>Reply to the message received.</summary>
        /// <param name="value">The value of the reply.</param>
        public void Reply(TReply value)
        {
            Value = value;
            CompleteEvent.Set();
        }

        /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
        public void Dispose()
        {
            ((IDisposable)CompleteEvent).Dispose();
        }
    }
}
هل كانت مفيدة؟

المحلول

في رمز المثال الخاص بك ، يمكنك التنفيذ PostWithAsyncReply من ناحية PostWithReply. هذا ليس مثاليًا ، لأنه يعني أنه عند الاتصال PostWithAsyncReply ويستغرق الممثل بعض الوقت للتعامل معها ، وهناك بالفعل خيط مربوطان: الشخص الذي ينفذ الممثل والآخر ينتظره. سيكون من الأفضل أن يكون لديك مؤشر ترابط واحد ينفذ الممثل ثم الاتصال بالاستدعاء في الحالة غير المتزامنة. (من الواضح في الحالة المتزامنة لا يوجد تجنب ربط خيطين).

تحديث:

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

الممثل أ مكالمات الممثل ب ، في انتظار الرد. من أجل التعامل مع الطلب ، يحتاج الممثل B إلى استدعاء الممثل C. لذا الآن تنتظر المواضيع A و B الوحيدة ، و C's هي الوحيدة التي تعطي وحدة المعالجة المركزية في الواقع أي عمل للقيام به. الكثير لمتعدد الخيوط! ولكن هذا ما تحصل عليه إذا انتظرت الإجابات طوال الوقت.

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

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

سيكون من الأفضل التنفيذ PostWithReply من ناحية PostWithAsyncReply, ، أي في الاتجاه المعاكس جولة. النسخة غير المتزامنة منخفضة المستوى. بناء على مثال القائم على المندوبين (لأنه يتضمن كتابة كود أقل!):

private bool InsertCoinImpl(int value) 
{
    // only accept dimes/10p/whatever it is in euros
    return (value == 10);
}

public void InsertCoin(int value, Action<bool> accepted)
{
    Submit(() => accepted(InsertCoinImpl(value)));
}

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

نأمل أن تكون الحاجة إلى الانتظار متزامنًا هي قضية الأقلية. ولكن عندما تحتاجه ، يمكن توفيره بواسطة طريقة المساعد ، والغرض العام تمامًا ولا يرتبط بأي ممثل أو نوع رسالة محددة:

public static T Wait<T>(Action<Action<T>> activity)
{
    T result = default(T);
    var finished = new EventWaitHandle(false, EventResetMode.AutoReset);

    activity(r =>
        {
            result = r;
            finished.Set();
        });

    finished.WaitOne();
    return result;
}

حتى الآن في ممثل آخر يمكننا القول:

bool accepted = Helpers.Wait<bool>(r => chocMachine.InsertCoin(5, r));

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

نعيدك الآن إلى برنامجنا المعتاد ...

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

abstract class Actor
{
    Queue<Action> _messages = new Queue<Action>();

    protected void Submit(Action action)
    {
        // take out a lock of course
        _messages.Enqueue(action);
    }

    // also a "run" that reads and executes the 
    // message delegates on background threads
}

الآن ممثل محدد مشتق يتبع هذا النمط:

class ChocolateMachineActor : Actor
{
    private void InsertCoinImpl(int value) 
    {
        // whatever...
    }

    public void InsertCoin(int value)
    {
        Submit(() => InsertCoinImpl(value));
    }
}

لذا لإرسال رسالة إلى الممثل ، يمكنك فقط الاتصال بالطرق العامة. الخاص Impl الطريقة تفعل العمل الحقيقي. لا حاجة لكتابة مجموعة من فئات الرسائل باليد.

من الواضح أنني تركت الأشياء حول الرد ، ولكن يمكن القيام كل ذلك بمزيد من المعلمات. (انظر التحديث أعلاه).

نصائح أخرى

لخص ستيف جيلهام كيف يتولى المترجم فعليًا النقابات المميزة. بالنسبة للرمز الخاص بك ، يمكنك النظر في نسخة مبسطة من ذلك. بالنظر إلى ما يلي f#:

type QueueMessage<T> = ClearMessage | TryDequeueMessage | EnqueueMessage of T

إليك طريقة واحدة لمحاكاةها في C#:

public enum MessageType { ClearMessage, TryDequeueMessage, EnqueueMessage }

public abstract class QueueMessage<T>
{
    // prevents unwanted subclassing
    private QueueMessage() { }

    public abstract MessageType MessageType { get; }

    /// <summary>
    /// Only applies to EnqueueMessages
    /// </summary>
    public abstract T Item { get; }

    public static QueueMessage<T> MakeClearMessage() { return new ClearMessage(); }
    public static QueueMessage<T> MakeTryDequeueMessage() { return new TryDequeueMessage(); }
    public static QueueMessage<T> MakeEnqueueMessage(T item) { return new EnqueueMessage(item); }


    private sealed class ClearMessage : QueueMessage<T>
    {
        public ClearMessage() { }

        public override MessageType MessageType
        {
            get { return MessageType.ClearMessage; }
        }

        /// <summary>
        /// Not implemented by this subclass
        /// </summary>
        public override T Item
        {
            get { throw new NotImplementedException(); }
        }
    }

    private sealed class TryDequeueMessage : QueueMessage<T>
    {
        public TryDequeueMessage() { }

        public override MessageType MessageType
        {
            get { return MessageType.TryDequeueMessage; }
        }

        /// <summary>
        /// Not implemented by this subclass
        /// </summary>
        public override T Item
        {
            get { throw new NotImplementedException(); }
        }
    }

    private sealed class EnqueueMessage : QueueMessage<T>
    {
        private T item;
        public EnqueueMessage(T item) { this.item = item; }

        public override MessageType MessageType
        {
            get { return MessageType.EnqueueMessage; }
        }

        /// <summary>
        /// Gets the item to be enqueued
        /// </summary>
        public override T Item { get { return item; } }
    }
}

الآن ، في الكود الذي يتم إعطاؤه QueueMessage, ، يمكنك تشغيل MessageType خاصية بدلاً من مطابقة الأنماط ، وتأكد من الوصول إلى Item الممتلكات فقط على EnqueueMessageس.

تعديل

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

public abstract class QueueMessage<T>
{
    // prevents unwanted subclassing
    private QueueMessage() { }

    public abstract TReturn Match<TReturn>(Func<TReturn> clearCase, Func<TReturn> tryDequeueCase, Func<T, TReturn> enqueueCase);

    public static QueueMessage<T> MakeClearMessage() { return new ClearMessage(); }
    public static QueueMessage<T> MakeTryDequeueMessage() { return new TryDequeueMessage(); }
    public static QueueMessage<T> MakeEnqueueMessage(T item) { return new EnqueueMessage(item); }

    private sealed class ClearMessage : QueueMessage<T>
    {
        public ClearMessage() { }

        public override TReturn Match<TReturn>(Func<TReturn> clearCase, Func<TReturn> tryDequeueCase, Func<T, TReturn> enqueueCase)
        {
            return clearCase();
        }
    }

    private sealed class TryDequeueMessage : QueueMessage<T>
    {
        public TryDequeueMessage() { }

        public override TReturn Match<TReturn>(Func<TReturn> clearCase, Func<TReturn> tryDequeueCase, Func<T, TReturn> enqueueCase)
        {
            return tryDequeueCase();
        }
    }

    private sealed class EnqueueMessage : QueueMessage<T>
    {
        private T item;
        public EnqueueMessage(T item) { this.item = item; }

        public override TReturn Match<TReturn>(Func<TReturn> clearCase, Func<TReturn> tryDequeueCase, Func<T, TReturn> enqueueCase)
        {
            return enqueueCase(item);
        }
    }
}

كنت تستخدم هذا الرمز مثل هذا:

public class MessageUserTest
{
    public void Use()
    {
        // your code to get a message here...
        QueueMessage<string> msg = null; 

        // emulate pattern matching, but without constructor names
        int i =
            msg.Match(
                clearCase:      () => -1,
                tryDequeueCase: () => -2,
                enqueueCase:     s =>  s.Length);
    }
}

أنواع النقابات وخريطة مطابقة الأنماط بشكل مباشر لنمط الزائر ، لقد نشرت هذا عدة مرات من قبل:

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

(تحذير ، رمز لم يتم اختباره إلى الأمام ، ولكن يجب أن يعطيك فكرة عن كيفية القيام به)

دعنا نقول أن لدينا شيء مثل هذا:

type msg =
    | Add of int
    | Sub of int
    | Query of ReplyChannel<int>


let rec counts = function
    | [] -> (0, 0, 0)
    | Add(_)::xs -> let (a, b, c) = counts xs in (a + 1, b, c)
    | Sub(_)::xs -> let (a, b, c) = counts xs in (a, b + 1, c)
    | Query(_)::xs -> let (a, b, c) = counts xs in (a, b, c + 1)

ينتهي بك الأمر مع رمز C# الضخم:

interface IMsgVisitor<T>
{
    T Visit(Add msg);
    T Visit(Sub msg);
    T Visit(Query msg);
}

abstract class Msg
{
    public abstract T Accept<T>(IMsgVistor<T> visitor)
}

class Add : Msg
{
    public readonly int Value;
    public Add(int value) { this.Value = value; }
    public override T Accept<T>(IMsgVisitor<T> visitor) { return visitor.Visit(this); }
}

class Sub : Msg
{
    public readonly int Value;
    public Add(int value) { this.Value = value; }
    public override T Accept<T>(IMsgVisitor<T> visitor) { return visitor.Visit(this); }
}

class Query : Msg
{
    public readonly ReplyChannel<int> Value;
    public Add(ReplyChannel<int> value) { this.Value = value; }
    public override T Accept<T>(IMsgVisitor<T> visitor) { return visitor.Visit(this); }
}

الآن كلما أردت أن تفعل شيئًا مع الرسالة ، تحتاج إلى تنفيذ زائر:

class MsgTypeCounter : IMsgVisitor<MsgTypeCounter>
{
    public readonly Tuple<int, int, int> State;    

    public MsgTypeCounter(Tuple<int, int, int> state) { this.State = state; }

    public MsgTypeCounter Visit(Add msg)
    {
        Console.WriteLine("got Add of " + msg.Value);
        return new MsgTypeCounter(Tuple.Create(1 + State.Item1, State.Item2, State.Item3));
    }

    public MsgTypeCounter Visit(Sub msg)
    {
        Console.WriteLine("got Sub of " + msg.Value);
        return new MsgTypeCounter(Tuple.Create(State.Item1, 1 + State.Item2, State.Item3));
    }

    public MsgTypeCounter Visit(Query msg)
    {
        Console.WriteLine("got Query of " + msg.Value);
        return new MsgTypeCounter(Tuple.Create(State.Item1, 1 + State.Item2, State.Item3));
    }
}

ثم أخيرًا يمكنك استخدامه مثل هذا:

var msgs = new Msg[] { new Add(1), new Add(3), new Sub(4), new ReplyChannel(null) };
var counts = msgs.Aggregate(new MsgTypeVisitor(Tuple.Create(0, 0, 0)),
    (acc, x) => x.Accept(acc)).State;

نعم ، إنها منفردة كما يبدو ، ولكن هذه هي الطريقة التي تمرر بها رسائل متعددة فئة بطريقة آمنة من النوع ، وهذا أيضًا هو السبب في أننا لا ننفذ النقابات في C# ؛)

لقطة طويلة ، ولكن على أي حال ..

أفترض أن الوحدة التمييزية هي F# لـ ADT (نوع البيانات التجريدية). مما يعني أن النوع يمكن أن يكون أحد الأشياء العديدة.

في حالة وجود اثنين ، يمكنك محاولة وضعه في فئة عامة بسيطة مع معلمتين من النوع:

 public struct DiscriminatedUnion<T1,T2>
 {   
      public DiscriminatedUnion(T1 t1) { value = t1; }
      public DiscriminatedUnion(T2 t1) { value = t2; }


      public static implicit operator T1(DiscriminatedUnion<T1,T2> du) {return (T1)du.value; }
      public static implicit operator T2(DiscriminatedUnion<T1,T2> du) {return (T2)du.value; }

      object value;
 }

لجعلها تعمل لمدة 3 أو أكثر ، نحتاج إلى تكرار هذه الفئة عدة مرات. أي شخص لديه حل للتحميل الزائد للوظيفة اعتمادًا على نوع وقت التشغيل؟

إذا كان لديك هذا

type internal Either<'a, 'b> =
  | Left of 'a
  | Right of 'b

في F# ، ثم C# ما يعادل CLR الذي تم إنشاؤه للفئة Either<'a, 'b> له أنواع داخلية مثل

internal  class _Left : Either<a, b>
{
     internal readonly a left1;
     internal _Left(a left1);
}

كل مع علامة ، getter وطريقة المصنع

internal const  int tag_Left = 0;
internal static  Either<a, b> Left(a Left1);
internal a Left1 {  get; }

بالإضافة إلى تمييز

internal int  Tag { get; }

وفريق من الأساليب لتنفيذ الواجهات IStructuralEquatable, IComparable, IStructuralComparable

هناك نوع الاتحاد التمييز الذي تم فحصه في وقت الترجمة على الاتحاد التمييزي في C#

private class ClearMessage
{
    public static readonly ClearMessage Instance = new ClearMessage();    
    private ClearMessage() { }
}

private class TryDequeueMessage 
{
    public static readonly TryDequeueMessage Instance = new TryDequeueMessage();    
    private TryDequeueMessage() { }
}

private class EnqueueMessage
{
    public TValue Item { get; private set; }    
    private EnqueueMessage(TValue item) { Item = item; }
}

يمكن أن يتم استخدام الاتحاد التمييز على النحو التالي:

// New file
// Create an alias
using Message = Union<ClearMessage, TryDequeueMessage, EnqueMessage>;

int ProcessMessage(Message msg)
{
   return Message.Match(
      clear => 1,
      dequeue => 2,
      enqueue => 3);
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top