ตัวอย่างเครื่องจักรง่าย ๆ ใน C #?


257

ปรับปรุง:

ขอบคุณอีกครั้งสำหรับตัวอย่างพวกเขาได้รับประโยชน์มากและด้วยสิ่งต่อไปนี้ฉันไม่ได้ตั้งใจจะเอาอะไรไปจากพวกเขา

ไม่ใช่ตัวอย่างที่ได้รับในขณะนี้เท่าที่ฉันเข้าใจพวกเขา & รัฐเครื่องจักรเพียงครึ่งหนึ่งของสิ่งที่เรามักจะเข้าใจโดยเครื่องรัฐ?
ในแง่ที่ว่าตัวอย่างทำการเปลี่ยนสถานะ แต่นั่นแสดงโดยการเปลี่ยนค่าของตัวแปรเท่านั้น (และอนุญาตให้มีการเปลี่ยนแปลงค่าที่แตกต่างกัน - ในสถานะที่แตกต่างกัน) ในขณะที่โดยปกติแล้วเครื่องรัฐควรเปลี่ยนพฤติกรรมของมัน ความรู้สึกของการอนุญาตให้การเปลี่ยนแปลงค่าที่แตกต่างกันสำหรับตัวแปรขึ้นอยู่กับสถานะ แต่ในความรู้สึกของการอนุญาตให้วิธีการที่แตกต่างกันจะถูกดำเนินการสำหรับรัฐที่แตกต่างกัน

หรือฉันมีความเข้าใจผิดเกี่ยวกับกลไกของรัฐและการใช้งานร่วมกัน?

ขอแสดงความนับถืออย่างสูง


คำถามเดิม:

ฉันพบการสนทนานี้เกี่ยวกับ state state & iterator block ใน c #และเครื่องมือในการสร้าง state machine และไม่ใช่สำหรับ C # ดังนั้นฉันจึงพบสิ่งที่เป็นนามธรรมมากมาย แต่เมื่อ noob ทั้งหมดนี้ค่อนข้างสับสนเล็กน้อย

ดังนั้นมันจะดีถ้ามีใครสามารถให้ซอร์สโค้ดตัวอย่าง C # ที่รู้ตัวว่าเป็นกลไกแบบง่าย ๆ ที่อาจมี 3,4 สถานะเพื่อให้ได้รับส่วนสำคัญของมัน



คุณสงสัยเกี่ยวกับเครื่องจักรของรัฐโดยทั่วไปหรือเป็นเพียงตัวทำซ้ำตาม
Skurmedel

2
มี. Net Core stateless lib พร้อมตัวอย่าง DAGs daigram และอื่น ๆ - ควรตรวจสอบ: hanselman.com/blog/…
zmische

คำตอบ:


416

เริ่มต้นด้วยแผนภาพสถานะอย่างง่ายนี้:

ไดอะแกรมเครื่องจักรแบบง่าย

เรามี:

  • 4 สถานะ (ไม่ใช้งาน, ใช้งาน, หยุดชั่วคราวและออกแล้ว)
  • การเปลี่ยนสถานะ 5 ประเภท (คำสั่งเริ่มต้น, คำสั่งปลายทาง, คำสั่งหยุดชั่วคราว, คำสั่ง Resume, คำสั่ง Exit)

คุณสามารถแปลงเป็น C # ได้หลายวิธีเช่นการทำคำสั่ง switch ในสถานะและคำสั่งปัจจุบันหรือค้นหาการเปลี่ยนแปลงในตารางการเปลี่ยนแปลง สำหรับเครื่องสถานะเรียบง่ายนี้ฉันชอบตารางการเปลี่ยนแปลงซึ่งง่ายต่อการแสดงโดยใช้Dictionary:

using System;
using System.Collections.Generic;

namespace Juliet
{
    public enum ProcessState
    {
        Inactive,
        Active,
        Paused,
        Terminated
    }

    public enum Command
    {
        Begin,
        End,
        Pause,
        Resume,
        Exit
    }

    public class Process
    {
        class StateTransition
        {
            readonly ProcessState CurrentState;
            readonly Command Command;

            public StateTransition(ProcessState currentState, Command command)
            {
                CurrentState = currentState;
                Command = command;
            }

            public override int GetHashCode()
            {
                return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                StateTransition other = obj as StateTransition;
                return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
            }
        }

        Dictionary<StateTransition, ProcessState> transitions;
        public ProcessState CurrentState { get; private set; }

        public Process()
        {
            CurrentState = ProcessState.Inactive;
            transitions = new Dictionary<StateTransition, ProcessState>
            {
                { new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
                { new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
                { new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
                { new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
                { new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
                { new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
            };
        }

        public ProcessState GetNext(Command command)
        {
            StateTransition transition = new StateTransition(CurrentState, command);
            ProcessState nextState;
            if (!transitions.TryGetValue(transition, out nextState))
                throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
            return nextState;
        }

        public ProcessState MoveNext(Command command)
        {
            CurrentState = GetNext(command);
            return CurrentState;
        }
    }


    public class Program
    {
        static void Main(string[] args)
        {
            Process p = new Process();
            Console.WriteLine("Current State = " + p.CurrentState);
            Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
            Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
            Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
            Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
            Console.ReadLine();
        }
    }
}

ในฐานะที่เป็นเรื่องของความชอบส่วนตัวฉันชอบที่จะออกแบบเครื่องจักรของรัฐด้วยGetNextฟังก์ชั่นเพื่อคืนสถานะถัดไปอย่างไม่แน่นอนและMoveNextฟังก์ชั่นในการกลายพันธุ์กลไกรัฐ


66
+1 สำหรับการใช้งานGetHashCode()primes ที่ถูกต้อง
ja72

13
คุณช่วยอธิบายวัตถุประสงค์ของ GetHashCode () ให้ฉันได้ไหม
Siddharth

14
@Siddharth: การใช้StateTransitionคลาสเป็นคีย์ในพจนานุกรมและความเท่าเทียมกันของคีย์นั้นมีความสำคัญ สองกรณีที่แตกต่างกันของStateTransitionควรได้รับการพิจารณาเท่ากับตราบเท่าที่พวกเขาเป็นตัวแทนของการเปลี่ยนแปลงเดียวกัน (เช่นCurrentStateและCommandเหมือนกัน) ความเท่าเทียมกันในการดำเนินการที่คุณต้องแทนที่เช่นเดียวกับEquals GetHashCodeโดยเฉพาะพจนานุกรมจะใช้รหัสแฮชและวัตถุที่เท่าเทียมกันสองรายการจะต้องส่งคืนรหัสแฮชเดียวกัน คุณจะได้รับประสิทธิภาพที่ดีหากวัตถุที่ไม่เท่ากันไม่มากเกินไปใช้รหัสแฮชเดียวกันซึ่งเป็นสาเหตุที่GetHashCodeนำมาใช้ตามที่แสดง
Martin Liversage

14
แม้ว่าสิ่งนี้จะทำให้คุณได้รับสถานะของเครื่องจักร (และการติดตั้ง C # 'ที่เหมาะสมเช่นกัน) ฉันรู้สึกว่ามันยังขาดคำตอบสำหรับคำถามของ OP เกี่ยวกับการเปลี่ยนแปลงพฤติกรรม? ท้ายที่สุดมันก็แค่คำนวณสถานะ แต่พฤติกรรมที่เกี่ยวข้องกับการเปลี่ยนแปลงสถานะเนื้อจริงของโปรแกรมและมักจะเรียกว่าเหตุการณ์เข้า / ออกยังคงหายไป
stijn

2
ถ้าใครสักคนที่ต้องการมัน: ฉันปรับเครื่องเทตนี้และใช้มันในเกมสามัคคีของฉัน มันมีอยู่ในศูนย์กลางคอมไพล์: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89

73

คุณอาจต้องการใช้หนึ่งใน Finite State Machines ที่มีอยู่ เช่น bbv.Common.StateMachine พบได้ที่http://code.google.com/p/bbvcommon/wiki/StateMachine มันมีไวยากรณ์ที่ใช้งานง่ายอย่างคล่องแคล่วและมีคุณสมบัติมากมายเช่นการกระทำเข้า / ออก, การดำเนินการเปลี่ยนผ่าน, การป้องกัน, ลำดับขั้น, การใช้งานแบบพาสซีฟ (ดำเนินการบนเธรดของผู้โทร) และการใช้งาน เหตุการณ์ถูกเพิ่มเข้าไปในคิว)

ยกตัวอย่าง Juliets คำจำกัดความสำหรับเครื่องสถานะได้ง่ายมาก:

var fsm = new PassiveStateMachine<ProcessState, Command>();
fsm.In(ProcessState.Inactive)
   .On(Command.Exit).Goto(ProcessState.Terminated).Execute(SomeTransitionAction)
   .On(Command.Begin).Goto(ProcessState.Active);
fsm.In(ProcessState.Active)
   .ExecuteOnEntry(SomeEntryAction)
   .ExecuteOnExit(SomeExitAction)
   .On(Command.End).Goto(ProcessState.Inactive)
   .On(Command.Pause).Goto(ProcessState.Paused);
fsm.In(ProcessState.Paused)
   .On(Command.End).Goto(ProcessState.Inactive).OnlyIf(SomeGuard)
   .On(Command.Resume).Goto(ProcessState.Active);
fsm.Initialize(ProcessState.Inactive);
fsm.Start();

fsm.Fire(Command.Begin);

อัปเดต : ที่ตั้งโครงการได้ย้ายไปที่: https://github.com/appccelerate/statemachine


4
ขอบคุณสำหรับการอ้างอิงเครื่องรัฐโอเพ่นซอร์สที่ยอดเยี่ยมนี้ ฉันจะถามวิธีรับสถานะปัจจุบันได้อย่างไร
Ramazan Polat

3
คุณทำไม่ได้และไม่ควรทำ รัฐเป็นสิ่งที่ไม่แน่นอน เมื่อคุณร้องขอสถานะเป็นไปได้ว่าคุณอยู่ในช่วงกลางของการเปลี่ยนแปลง การกระทำทั้งหมดควรกระทำภายในช่วงการเปลี่ยนผ่านการเข้าสู่สถานะและออกจากรัฐ หากคุณต้องการมีสถานะจริงๆคุณสามารถเพิ่มเขตโลคัลและกำหนดสถานะในการดำเนินการรายการ
Remo Gloor

4
คำถามสำหรับสิ่งที่คุณ "ต้องการ" มันและถ้าคุณต้องการรัฐ SM หรือรัฐชนิดอื่น ๆ เช่นหากคุณต้องการข้อความที่แสดงดังนั้นข้อความที่ระบุไว้หลายข้อความอาจมีข้อความที่แสดงเช่นเดียวกันหากการเตรียมการส่งมีหลายสถานะย่อย ในกรณีนี้คุณควรทำสิ่งที่คุณตั้งใจจะทำ อัปเดตข้อความที่แสดงในตำแหน่งที่ถูกต้อง เช่นภายใน ExecuteOnEntry หากคุณต้องการข้อมูลเพิ่มเติมให้ถามคำถามใหม่และระบุปัญหาของคุณให้ถูกต้องเนื่องจากหัวข้อนี้กำลังจะปิดตัวลงที่นี่
Remo Gloor

ตกลงฉันกำลังถามคำถามใหม่และรอให้คุณตอบกลับ เพราะฉันไม่คิดว่าจะมีใครแก้ปัญหานี้ได้อีกเพราะคุณมีคำตอบที่ดีที่สุด แต่ผู้ถามยังไม่ยอมรับ ฉันจะโพสต์ URL คำถามที่นี่ ขอบคุณ
Ramazan Polat

4
+1 สำหรับ API ที่คล่องแคล่วและมีการเปิดเผย มันเจ๋งมาก. BTW ดูเหมือนว่าโค้ด google จะล้าสมัย ไซต์โครงการใหม่ล่าสุดของพวกเขาอยู่ที่ GitHub ที่นี่
KFL

51

นี่คือตัวอย่างของเครื่องสถานะ จำกัด แบบคลาสสิกการสร้างแบบจำลองอุปกรณ์อิเล็กทรอนิกส์ที่ง่ายมาก (เช่นโทรทัศน์)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace fsm
{
class Program
{
    static void Main(string[] args)
    {
        var fsm = new FiniteStateMachine();
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.PlugIn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOff);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.RemovePower);
        Console.WriteLine(fsm.State);
        Console.ReadKey();
    }

    class FiniteStateMachine
    {
        public enum States { Start, Standby, On };
        public States State { get; set; }

        public enum Events { PlugIn, TurnOn, TurnOff, RemovePower };

        private Action[,] fsm;

        public FiniteStateMachine()
        {
            this.fsm = new Action[3, 4] { 
                //PlugIn,       TurnOn,                 TurnOff,            RemovePower
                {this.PowerOn,  null,                   null,               null},              //start
                {null,          this.StandbyWhenOff,    null,               this.PowerOff},     //standby
                {null,          null,                   this.StandbyWhenOn, this.PowerOff} };   //on
        }
        public void ProcessEvent(Events theEvent)
        {
            this.fsm[(int)this.State, (int)theEvent].Invoke();
        }

        private void PowerOn() { this.State = States.Standby; }
        private void PowerOff() { this.State = States.Start; }
        private void StandbyWhenOn() { this.State = States.Standby; }
        private void StandbyWhenOff() { this.State = States.On; }
    }
}
}

6
สำหรับทุกคนที่เพิ่งเริ่มใช้เครื่องจักรของรัฐนี่เป็นตัวอย่างแรกที่ยอดเยี่ยมในการทำให้เท้าของคุณเปียกก่อน
PositiveGuy

2
ฉันใหม่สำหรับเครื่องจักรของรัฐและจริงจังนี่ทำให้ฉัน The Light - ขอบคุณ!
MC5

1
ฉันชอบการนำไปใช้นี้ สำหรับใครก็ตามที่อาจสะดุดกับเรื่องนี้การปรับปรุงเล็กน้อย ในชั้นเรียน FSM ฉันเพิ่มprivate void DoNothing() {return;}และแทนที่ทุกกรณี null this.DoNothingกับ มีผลข้างเคียงที่น่าพอใจจากการคืนสถานะปัจจุบัน
Sethmo011

1
ฉันสงสัยว่ามีเหตุผลเบื้องหลังชื่อเหล่านี้หรือไม่ เมื่อฉันมองไปที่นี้สัญชาตญาณครั้งแรกของฉันคือการเปลี่ยนชื่อองค์ประกอบของการStates Unpowered, Standby, Onเหตุผลของฉันคือถ้ามีคนถามฉันว่าโทรทัศน์ของฉันอยู่ในสถานะใดฉันจะพูดว่า "ปิด" ไม่ใช่ "เริ่ม" ฉันยังมีการเปลี่ยนแปลงStandbyWhenOnและStandbyWhenOffการและTurnOn TurnOffนั่นทำให้รหัสอ่านง่ายขึ้น แต่ฉันสงสัยว่ามีอนุสัญญาหรือปัจจัยอื่น ๆ ที่ทำให้คำศัพท์ของฉันไม่เหมาะสมหรือไม่
Jason Hamje

ดูเหมือนว่าสมเหตุสมผลฉันไม่ได้ทำตามแบบแผนการตั้งชื่อของรัฐจริงๆ ชื่อที่เหมาะสมสำหรับสิ่งที่คุณทำโมเดล
Pete Stensønes

20

การโปรโมตตนเองที่ไร้ยางอายที่นี่ แต่เมื่อไม่นานมานี้ฉันได้สร้างห้องสมุดชื่อYieldMachineซึ่งช่วยให้เครื่องสถานะที่มีความซับซ้อน จำกัด สามารถอธิบายได้อย่างสะอาดและเรียบง่าย ตัวอย่างเช่นพิจารณาโคมไฟ:

เครื่องรัฐของหลอดไฟ

ขอให้สังเกตว่าเครื่องสถานะนี้มี 2 ทริกเกอร์และ 3 สถานะ ในรหัส YieldMachine เราเขียนวิธีการเดียวสำหรับพฤติกรรมที่เกี่ยวข้องกับสถานะทั้งหมดซึ่งเราใช้ความโหดร้ายที่น่ากลัวในการใช้gotoสำหรับแต่ละรัฐ ทริกเกอร์กลายเป็นสถานที่ให้บริการหรือสาขาประเภทตกแต่งด้วยแอตทริบิวต์ที่เรียกว่าAction Triggerฉันได้แสดงความคิดเห็นรหัสของสถานะแรกและช่วงการเปลี่ยนภาพด้านล่าง สถานะถัดไปเป็นไปตามรูปแบบเดียวกัน

public class Lamp : StateMachine
{
    // Triggers (or events, or actions, whatever) that our
    // state machine understands.
    [Trigger]
    public readonly Action PressSwitch;

    [Trigger]
    public readonly Action GotError;

    // Actual state machine logic
    protected override IEnumerable WalkStates()
    {
    off:                                       
        Console.WriteLine("off.");
        yield return null;

        if (Trigger == PressSwitch) goto on;
        InvalidTrigger();

    on:
        Console.WriteLine("*shiiine!*");
        yield return null;

        if (Trigger == GotError) goto error;
        if (Trigger == PressSwitch) goto off;
        InvalidTrigger();

    error:
        Console.WriteLine("-err-");
        yield return null;

        if (Trigger == PressSwitch) goto off;
        InvalidTrigger();
    }
}

สั้นและดีเอ๊ะ!

เครื่องสถานะนี้ถูกควบคุมเพียงแค่ส่งทริกเกอร์ไปที่มัน:

var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off

sm.PressSwitch(); //go on
sm.GotError();    //get error
sm.PressSwitch(); //go off

เพื่ออธิบายให้ชัดเจนฉันได้เพิ่มความคิดเห็นลงในสถานะแรกเพื่อช่วยให้คุณเข้าใจวิธีใช้สิ่งนี้

    protected override IEnumerable WalkStates()
    {
    off:                                       // Each goto label is a state

        Console.WriteLine("off.");             // State entry actions

        yield return null;                     // This means "Wait until a 
                                               // trigger is called"

                                               // Ah, we got triggered! 
                                               //   perform state exit actions 
                                               //   (none, in this case)

        if (Trigger == PressSwitch) goto on;   // Transitions go here: 
                                               // depending on the trigger 
                                               // that was called, go to
                                               // the right state

        InvalidTrigger();                      // Throw exception on 
                                               // invalid trigger

        ...

ใช้งานได้เนื่องจากคอมไพเลอร์ C # สร้างเครื่องสถานะภายในสำหรับแต่ละวิธีที่ใช้ yield returnสร้างขึ้นจริงเครื่องรัฐภายในสำหรับแต่ละวิธีที่ใช้ โครงสร้างนี้มักใช้เพื่อสร้างลำดับของข้อมูลอย่างเกียจคร้าน แต่ในกรณีนี้เราไม่สนใจลำดับที่ส่งคืน (ซึ่งเป็นโมฆะทั้งหมดแล้ว) แต่ในพฤติกรรมของรัฐที่สร้างขึ้นภายใต้ประทุน

StateMachineชั้นฐานไม่สะท้อนบางอย่างเกี่ยวกับการก่อสร้างเพื่อกำหนดรหัสให้กับแต่ละ[Trigger]การกระทำที่กำหนดTriggerสมาชิกและย้ายเครื่องของรัฐไปข้างหน้า

แต่คุณไม่จำเป็นต้องเข้าใจภายในเพื่อให้สามารถใช้งานได้


2
"โกโตะ" นั้นโหดร้ายก็ต่อเมื่อมันกระโดดไปมาระหว่างวิธีการต่างๆ โชคดีที่ไม่อนุญาตให้ใช้ใน C #
Brannon

จุดดี! ในความเป็นจริงฉันจะรู้สึกประทับใจมากหากภาษาที่พิมพ์แบบคงที่ใด ๆ จะจัดการเพื่อให้gotoวิธีการระหว่าง
skrebbel

3
@ Brannon: ภาษาใดที่อนุญาตให้gotoข้ามระหว่างเมธอดได้ ฉันไม่เห็นว่ามันจะทำงานอย่างไร ไม่มีgotoเป็นปัญหาเพราะมันจะส่งผลในการเขียนโปรแกรมขั้นตอน (นี้ด้วยตัวเองมีความซับซ้อนสิ่งที่ดีเช่นการทดสอบหน่วย) ส่งเสริมรหัสซ้ำ (สังเกตเห็นว่าInvalidTriggerความต้องการที่จะแทรกสำหรับทุกรัฐ?) และในที่สุดก็ทำให้โปรแกรมไหลยากที่จะติดตาม เปรียบเทียบสิ่งนี้กับโซลูชันอื่น ๆ (ส่วนใหญ่) ในชุดข้อความนี้และคุณจะเห็นว่านี่เป็นวิธีเดียวที่ FSM ทั้งหมดเกิดขึ้นในวิธีการเดียว ซึ่งเพียงพอที่จะแจ้งข้อกังวล
Groo

1
ตัวอย่างเช่น @Groo, GW-BASIC ช่วยให้ไม่มีวิธีการหรือฟังก์ชั่น นอกจากนั้นฉันมีเวลายากมากที่จะเข้าใจว่าทำไมคุณถึงพบว่า "โปรแกรมไหลตามยากกว่า" ในตัวอย่างนี้ มันเป็นกลไกของรัฐ "ไปที่" สถานะจากอีกอันหนึ่งเป็นสิ่งเดียวที่คุณทำ แผนที่นี้gotoค่อนข้างดี
skrebbel

3
GW-BASIC อนุญาตให้gotoข้ามระหว่างฟังก์ชั่น แต่ไม่รองรับฟังก์ชั่น? :) คุณพูดถูกแล้วคำว่า "ยากที่จะทำตาม" นั้นเป็นgotoปัญหาทั่วไปมากกว่าไม่ใช่ปัญหาในกรณีนี้
Groo

13

คุณสามารถโค้ดบล็อกตัววนซ้ำที่ให้คุณเรียกใช้บล็อกโค้ดในแบบที่มีการเตรียมการ การแบ่งรหัสบล็อกไม่จำเป็นต้องสอดคล้องกับสิ่งใดจริงๆมันเป็นเพียงวิธีที่คุณต้องการโค้ด ตัวอย่างเช่น:

IEnumerable<int> CountToTen()
{
    System.Console.WriteLine("1");
    yield return 0;
    System.Console.WriteLine("2");
    System.Console.WriteLine("3");
    System.Console.WriteLine("4");
    yield return 0;
    System.Console.WriteLine("5");
    System.Console.WriteLine("6");
    System.Console.WriteLine("7");
    yield return 0;
    System.Console.WriteLine("8");
    yield return 0;
    System.Console.WriteLine("9");
    System.Console.WriteLine("10");
}

ในกรณีนี้เมื่อคุณโทรไปที่ CountToTen ยังไม่มีการดำเนินการใด ๆ สิ่งที่คุณได้รับคือเครื่องกำเนิดไฟฟ้าอย่างมีประสิทธิภาพซึ่งคุณสามารถสร้างอินสแตนซ์ใหม่ของเครื่องรัฐได้ คุณทำได้โดยโทรหา GetEnumerator () IEnumerator ที่ได้นั้นเป็นเครื่องสถานะที่คุณสามารถขับได้โดยการเรียก MoveNext (... )

ดังนั้นในตัวอย่างนี้ครั้งแรกที่คุณเรียกใช้ MoveNext (... ) คุณจะเห็น "1" เขียนไปยังคอนโซลและครั้งต่อไปที่คุณเรียกใช้ MoveNext (... ) คุณจะเห็น 2, 3, 4 และ 5, 6, 7 และ 8 และ 9, 10 อย่างที่คุณเห็นมันเป็นกลไกที่มีประโยชน์สำหรับการวางแผนว่าจะเกิดอะไรขึ้น


6
การเชื่อมโยงหน้าที่ที่จะเตือนยุติธรรม
sehe

8

ฉันโพสต์คำตอบอื่นที่นี่เนื่องจากเป็นเครื่องรัฐจากมุมมองที่แตกต่างกัน; ภาพมาก

คำตอบเดิมของฉันคือรหัสบังคับคลาสสิก ฉันคิดว่ามันเป็นภาพที่ค่อนข้างเป็นรหัสไปเพราะอาร์เรย์ซึ่งทำให้การมองเห็นเครื่องรัฐง่าย ข้อเสียคือคุณต้องเขียนทั้งหมดนี้ Remosคำตอบของช่วยลดความพยายามในการเขียนรหัสจานหม้อไอน้ำ แต่มองเห็นได้ไกลน้อยกว่า มีทางเลือกที่สาม; วาดเครื่องรัฐจริงๆ

หากคุณกำลังใช้ NET และสามารถกำหนดเป้าหมายรุ่นที่ 4 ของเวลาทำงานแล้วคุณมีตัวเลือกของการใช้กิจกรรมเครื่องรัฐเวิร์กโฟลว์ของ สิ่งสำคัญเหล่านี้ช่วยให้คุณสามารถวาดกลไกสถานะ (เหมือนในจูเลียต)แผนภาพของ ) และให้เวลารันไทม์ WF ดำเนินการให้คุณ

ดูบทความ MSDN การสร้างเครื่องสถานะด้วย Windows Workflow Foundationสำหรับรายละเอียดเพิ่มเติมและไซต์ CodePlex นี้สำหรับรุ่นล่าสุด

นั่นเป็นตัวเลือกที่ฉันต้องการเสมอเมื่อกำหนดเป้าหมาย. NET เพราะง่ายต่อการดูเปลี่ยนแปลงและอธิบายต่อผู้ที่ไม่ใช่โปรแกรมเมอร์ รูปภาพมีค่าหนึ่งพันคำเช่นที่พวกเขาพูด!


ฉันคิดว่ากลไกสถานะเป็นหนึ่งในส่วนที่ดีที่สุดของรากฐานกระบวนการทำงานทั้งหมด!
fabsenet

7

มีประโยชน์ที่ต้องจำไว้ว่าเครื่องจักรของรัฐนั้นเป็นนามธรรมและคุณไม่จำเป็นต้องใช้เครื่องมือใดเป็นพิเศษในการสร้าง แต่เครื่องมือนั้นมีประโยชน์

ตัวอย่างเช่นคุณสามารถรับรู้สถานะเครื่องจักรที่มีฟังก์ชั่น:

void Hunt(IList<Gull> gulls)
{
    if (gulls.Empty())
       return;

    var target = gulls.First();
    TargetAcquired(target, gulls);
}

void TargetAcquired(Gull target, IList<Gull> gulls)
{
    var balloon = new WaterBalloon(weightKg: 20);

    this.Cannon.Fire(balloon);

    if (balloon.Hit)
    {
       TargetHit(target, gulls);
    }
    else
       TargetMissed(target, gulls);
}

void TargetHit(Gull target, IList<Gull> gulls)
{
    Console.WriteLine("Suck on it {0}!", target.Name);
    Hunt(gulls);
}

void TargetMissed(Gull target, IList<Gull> gulls)
{
    Console.WriteLine("I'll get ya!");
    TargetAcquired(target, gulls);
}

เครื่องนี้จะล่านกนางนวลและพยายามตีด้วยลูกโป่งน้ำ หากพลาดก็จะลองทำการยิงจนกว่าจะเจอ (สามารถทำได้ด้วยความคาดหวังที่เป็นจริง;)) มิฉะนั้นมันก็จะตกอยู่ในคอนโซล มันยังล่าต่อไปจนกว่ามันจะออกนอกรังควาน

แต่ละฟังก์ชั่นสอดคล้องกับแต่ละรัฐ; สถานะเริ่มต้นและสิ้นสุด (หรือยอมรับ ) จะไม่ปรากฏขึ้น อาจมีสถานะมากกว่านี้ในการจำลองโดยฟังก์ชั่น ตัวอย่างเช่นหลังจากการยิงบอลลูนเครื่องอยู่ในสถานะอื่นมากกว่าที่เคยเป็นมาก่อนหน้านี้ แต่ฉันตัดสินใจว่าความแตกต่างนี้ไม่สามารถทำได้

วิธีทั่วไปคือการใช้คลาสเพื่อเป็นตัวแทนของรัฐแล้วเชื่อมต่อพวกเขาในรูปแบบที่แตกต่างกัน


7

พบบทเรียนออนไลน์ที่ยอดเยี่ยมนี้และมันช่วยฉันห่อหัวของฉันรอบ ๆ เครื่องสถานะ จำกัด

http://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867

การสอนเป็นภาษาที่ไม่เชื่อเรื่องพระเจ้าดังนั้นจึงสามารถปรับให้เข้ากับความต้องการของ C # ของคุณได้อย่างง่ายดาย

ตัวอย่างที่ใช้ (มดที่กำลังมองหาอาหาร) นั้นเข้าใจง่าย


จากบทช่วยสอน:

ป้อนคำอธิบายรูปภาพที่นี่

public class FSM {
    private var activeState :Function; // points to the currently active state function

    public function FSM() {
    }

    public function setState(state :Function) :void {
        activeState = state;
    }

    public function update() :void {
        if (activeState != null) {
            activeState();
        }
    }
}


public class Ant
{
    public var position   :Vector3D;
    public var velocity   :Vector3D;
    public var brain      :FSM;

    public function Ant(posX :Number, posY :Number) {
        position    = new Vector3D(posX, posY);
        velocity    = new Vector3D( -1, -1);
        brain       = new FSM();

        // Tell the brain to start looking for the leaf.
        brain.setState(findLeaf);
    }

    /**
    * The "findLeaf" state.
    * It makes the ant move towards the leaf.
    */
    public function findLeaf() :void {
        // Move the ant towards the leaf.
        velocity = new Vector3D(Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y);

        if (distance(Game.instance.leaf, this) <= 10) {
            // The ant is extremelly close to the leaf, it's time
            // to go home.
            brain.setState(goHome);
        }

        if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) {
            // Mouse cursor is threatening us. Let's run away!
            // It will make the brain start calling runAway() from
            // now on.
            brain.setState(runAway);
        }
    }

    /**
    * The "goHome" state.
    * It makes the ant move towards its home.
    */
    public function goHome() :void {
        // Move the ant towards home
        velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y);

        if (distance(Game.instance.home, this) <= 10) {
            // The ant is home, let's find the leaf again.
            brain.setState(findLeaf);
        }
    }

    /**
    * The "runAway" state.
    * It makes the ant run away from the mouse cursor.
    */
    public function runAway() :void {
        // Move the ant away from the mouse cursor
        velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y);

        // Is the mouse cursor still close?
        if (distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) {
            // No, the mouse cursor has gone away. Let's go back looking for the leaf.
            brain.setState(findLeaf);
        }
    }

    public function update():void {
        // Update the FSM controlling the "brain". It will invoke the currently
        // active state function: findLeaf(), goHome() or runAway().
        brain.update();

        // Apply the velocity vector to the position, making the ant move.
        moveBasedOnVelocity();
    }

    (...)
}

1
ในขณะที่ลิงค์นี้อาจตอบคำถามได้ดีกว่าที่จะรวมส่วนสำคัญของคำตอบที่นี่และให้ลิงค์สำหรับการอ้างอิง คำตอบสำหรับลิงค์เท่านั้นอาจไม่ถูกต้องหากหน้าเว็บที่เชื่อมโยงมีการเปลี่ยนแปลง - จากการรีวิว
drneel

@drneel ฉันสามารถคัดลอกและวางบิตจากการกวดวิชา ... แต่นั่นจะไม่ได้รับเครดิตจากผู้เขียน?
Jet Blue

1
@JetBlue: ปล่อยลิงก์ไว้ในคำตอบเพื่ออ้างอิงและรวมบิตที่เกี่ยวข้องไว้ในคำของคุณในโพสต์คำตอบเพื่อไม่ให้ละเมิดลิขสิทธิ์ของใคร ฉันรู้ว่ามันดูเข้มงวด แต่มีคำตอบมากมายที่ดีกว่ามากเพราะกฎนี้
Flimm

6

วันนี้ฉันลึกลงไปในรูปแบบการออกแบบของรัฐ ฉันทำและทดสอบ ThreadState ซึ่งเท่ากับ (+/-) กับ Threading ใน C # ดังอธิบายในภาพจากThreading ใน C #

ป้อนคำอธิบายรูปภาพที่นี่

คุณสามารถเพิ่มสถานะใหม่ ๆ ได้ง่าย ๆ กำหนดค่าการย้ายจากรัฐหนึ่งไปอีกรัฐเป็นเรื่องง่ายมากเพราะมันอยู่ในการดำเนินการของรัฐ

การใช้งานและการใช้ที่: ใช้. NET ThreadState ตามรูปแบบการออกแบบของรัฐ


1
ลิงค์เสียชีวิต คุณมีอีกไหม
ม้วน

5

ฉันยังไม่ได้ลองใช้งาน FSM ใน C # แต่สิ่งเหล่านี้เสียง (หรือดู) ซับซ้อนมากกับวิธีที่ฉันจัดการ FSM ในอดีตในภาษาระดับต่ำเช่น C หรือ ASM

ฉันเชื่อว่าวิธีการที่ฉันรู้จักเสมอเรียกว่า "วนซ้ำ" ในนั้นคุณจะต้องวนรอบ 'while' ที่ออกเป็นระยะ ๆ ตามเหตุการณ์ (ขัดจังหวะ) จากนั้นกลับสู่วงหลักอีกครั้ง

ภายในตัวจัดการขัดจังหวะคุณจะผ่าน CurrentState และส่งกลับ NextState ซึ่งจะเขียนทับตัวแปร CurrentState ในลูปหลัก คุณทำโฆษณานี้ไม่มีที่สิ้นสุดจนกว่าโปรแกรมจะปิด (หรือรีเซ็ตไมโครคอนโทรลเลอร์)

สิ่งที่ฉันเห็นคำตอบอื่น ๆ ทั้งหมดดูซับซ้อนมากเมื่อเทียบกับที่ FSM ตั้งใจไว้ว่าจะนำไปใช้ ความสวยงามของมันอยู่ในความเรียบง่ายและ FSM นั้นซับซ้อนมากกับหลาย ๆ รัฐและช่วงการเปลี่ยนภาพ แต่พวกเขาก็อนุญาตให้กระบวนการที่ซับซ้อนสามารถแยกย่อยสลาย

ฉันรู้ว่าคำตอบของฉันไม่ควรมีคำถามอื่น แต่ฉันถูกบังคับให้ถาม: ทำไมวิธีแก้ปัญหาที่เสนออื่น ๆ เหล่านี้จึงมีความซับซ้อน
ดูเหมือนว่าพวกมันจะคล้ายกับการตอกตะปูขนาดเล็กด้วยค้อนเลื่อนขนาดใหญ่


1
เห็นด้วยอย่างสมบูรณ์ ง่าย ๆ ในขณะที่วนรอบพร้อมกับคำสั่งเปลี่ยนเป็นเรื่องง่ายเท่าที่คุณจะได้รับ
ม้วน

2
หากคุณไม่มีเครื่องสถานะที่ซับซ้อนมากที่มีสถานะและเงื่อนไขต่าง ๆ มากมายซึ่งคุณจะพบกับสวิตช์ซ้อนกันหลายตัว นอกจากนี้อาจมีบทลงโทษในการรอไม่ว่างขึ้นอยู่กับการใช้งานลูปของคุณ
Sune Rievers

3

รูปแบบการแข่งขันคืออะไร สิ่งนี้ตรงกับความต้องการของคุณหรือไม่?

ฉันคิดว่าบริบทของมันเกี่ยวข้อง แต่ก็คุ้มค่าที่จะถ่ายแน่นอน

http://en.wikipedia.org/wiki/State_pattern

สิ่งนี้ปล่อยให้รัฐของคุณตัดสินใจว่าจะไปที่ไหนและไม่ใช่คลาส "วัตถุ"

บรูโน่


1
รูปแบบสถานะเกี่ยวข้องกับคลาสที่สามารถดำเนินการแตกต่างกันไปตามสถานะ / โหมดที่มีอยู่ในนั้นจะไม่จัดการกับการเปลี่ยนระหว่างรัฐ
Eli Algranti

3

ในความคิดของฉันเครื่องรัฐไม่ได้มีไว้สำหรับการเปลี่ยนแปลงสถานะเท่านั้น แต่ยังสำคัญมากสำหรับการจัดการทริกเกอร์ / เหตุการณ์ภายในสถานะที่ระบุ หากคุณต้องการเข้าใจรูปแบบการออกแบบเครื่องให้ดีขึ้นคำอธิบายที่ดีสามารถพบได้ในหนังสือHead First Design Patterns, หน้า 320320

ไม่เพียงเกี่ยวกับสถานะภายในตัวแปรเท่านั้น แต่ยังเกี่ยวกับการจัดการทริกเกอร์ภายในสถานะที่แตกต่างกัน บทที่ยอดเยี่ยม (และไม่ไม่มีค่าธรรมเนียมสำหรับฉันในการกล่าวถึงนี้ :-) ซึ่งมีเพียงคำอธิบายที่เข้าใจง่าย


3

ฉันเพิ่งได้รับสิ่งนี้:

https://code.google.com/p/ysharp/source/browse/#svn%2Ftrunk%2FStateMachinesPoC

นี่คือหนึ่งในตัวอย่างที่สาธิตการส่งคำสั่งโดยตรงและโดยอ้อมด้วยสถานะเป็น IObserver (สัญญาณ) ดังนั้นผู้ตอบสนองไปยังแหล่งสัญญาณ IObservable (ของสัญญาณ):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    using Machines;

    public static class WatchingTvSampleAdvanced
    {
        // Enum type for the transition triggers (instead of System.String) :
        public enum TvOperation { Plug, SwitchOn, SwitchOff, Unplug, Dispose }

        // The state machine class type is also used as the type for its possible states constants :
        public class Television : NamedState<Television, TvOperation, DateTime>
        {
            // Declare all the possible states constants :
            public static readonly Television Unplugged = new Television("(Unplugged TV)");
            public static readonly Television Off = new Television("(TV Off)");
            public static readonly Television On = new Television("(TV On)");
            public static readonly Television Disposed = new Television("(Disposed TV)");

            // For convenience, enter the default start state when the parameterless constructor executes :
            public Television() : this(Television.Unplugged) { }

            // To create a state machine instance, with a given start state :
            private Television(Television value) : this(null, value) { }

            // To create a possible state constant :
            private Television(string moniker) : this(moniker, null) { }

            private Television(string moniker, Television value)
            {
                if (moniker == null)
                {
                    // Build the state graph programmatically
                    // (instead of declaratively via custom attributes) :
                    Handler<Television, TvOperation, DateTime> stateChangeHandler = StateChange;
                    Build
                    (
                        new[]
                        {
                            new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = stateChangeHandler },
                            new { From = Television.Unplugged, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.SwitchOn, Goto = Television.On, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.SwitchOff, Goto = Television.Off, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler }
                        },
                        false
                    );
                }
                else
                    // Name the state constant :
                    Moniker = moniker;
                Start(value ?? this);
            }

            // Because the states' value domain is a reference type, disallow the null value for any start state value : 
            protected override void OnStart(Television value)
            {
                if (value == null)
                    throw new ArgumentNullException("value", "cannot be null");
            }

            // When reaching a final state, unsubscribe from all the signal source(s), if any :
            protected override void OnComplete(bool stateComplete)
            {
                // Holds during all transitions into a final state
                // (i.e., stateComplete implies IsFinal) :
                System.Diagnostics.Debug.Assert(!stateComplete || IsFinal);

                if (stateComplete)
                    UnsubscribeFromAll();
            }

            // Executed before and after every state transition :
            private void StateChange(IState<Television> state, ExecutionStep step, Television value, TvOperation info, DateTime args)
            {
                // Holds during all possible transitions defined in the state graph
                // (i.e., (step equals ExecutionStep.LeaveState) implies (not state.IsFinal))
                System.Diagnostics.Debug.Assert((step != ExecutionStep.LeaveState) || !state.IsFinal);

                // Holds in instance (i.e., non-static) transition handlers like this one :
                System.Diagnostics.Debug.Assert(this == state);

                switch (step)
                {
                    case ExecutionStep.LeaveState:
                        var timeStamp = ((args != default(DateTime)) ? String.Format("\t\t(@ {0})", args) : String.Empty);
                        Console.WriteLine();
                        // 'value' is the state value that we are transitioning TO :
                        Console.WriteLine("\tLeave :\t{0} -- {1} -> {2}{3}", this, info, value, timeStamp);
                        break;
                    case ExecutionStep.EnterState:
                        // 'value' is the state value that we have transitioned FROM :
                        Console.WriteLine("\tEnter :\t{0} -- {1} -> {2}", value, info, this);
                        break;
                    default:
                        break;
                }
            }

            public override string ToString() { return (IsConstant ? Moniker : Value.ToString()); }
        }

        public static void Run()
        {
            Console.Clear();

            // Create a signal source instance (here, a.k.a. "remote control") that implements
            // IObservable<TvOperation> and IObservable<KeyValuePair<TvOperation, DateTime>> :
            var remote = new SignalSource<TvOperation, DateTime>();

            // Create a television state machine instance (automatically set in a default start state),
            // and make it subscribe to a compatible signal source, such as the remote control, precisely :
            var tv = new Television().Using(remote);
            bool done;

            // Always holds, assuming the call to Using(...) didn't throw an exception (in case of subscription failure) :
            System.Diagnostics.Debug.Assert(tv != null, "There's a bug somewhere: this message should never be displayed!");

            // As commonly done, we can trigger a transition directly on the state machine :
            tv.MoveNext(TvOperation.Plug, DateTime.Now);

            // Alternatively, we can also trigger transitions by emitting from the signal source / remote control
            // that the state machine subscribed to / is an observer of :
            remote.Emit(TvOperation.SwitchOn, DateTime.Now);
            remote.Emit(TvOperation.SwitchOff);
            remote.Emit(TvOperation.SwitchOn);
            remote.Emit(TvOperation.SwitchOff, DateTime.Now);

            done =
                (
                    tv.
                        MoveNext(TvOperation.Unplug).
                        MoveNext(TvOperation.Dispose) // MoveNext(...) returns null iff tv.IsFinal == true
                    == null
                );

            remote.Emit(TvOperation.Unplug); // Ignored by the state machine thanks to the OnComplete(...) override above

            Console.WriteLine();
            Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);

            Console.WriteLine();
            Console.WriteLine("Press any key...");
            Console.ReadKey();
        }
    }
}

หมายเหตุ: ตัวอย่างนี้เป็นของเทียมและส่วนใหญ่มีไว้เพื่อสาธิตคุณลักษณะหลายอย่างของมุมฉาก ไม่ควรมีความจำเป็นจริง ๆ ในการใช้โดเมนค่าสถานะของตัวเองโดยคลาสแบบเต็มเป่าใช้ CRTP (ดู: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) เช่นนี้

นี่คือกรณีการใช้งานการใช้งานที่ง่ายและมีแนวโน้มมากขึ้นอย่างแน่นอน (โดยใช้ enum แบบง่าย ๆ เป็นโดเมนค่าสถานะ) สำหรับเครื่องสถานะเดียวกันและกรณีทดสอบเดียวกัน:

https://code.google.com/p/ysharp/source/browse/trunk/StateMachinesPoC/WatchingTVSample.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    using Machines;

    public static class WatchingTvSample
    {
        public enum Status { Unplugged, Off, On, Disposed }

        public class DeviceTransitionAttribute : TransitionAttribute
        {
            public Status From { get; set; }
            public string When { get; set; }
            public Status Goto { get; set; }
            public object With { get; set; }
        }

        // State<Status> is a shortcut for / derived from State<Status, string>,
        // which in turn is a shortcut for / derived from State<Status, string, object> :
        public class Device : State<Status>
        {
            // Executed before and after every state transition :
            protected override void OnChange(ExecutionStep step, Status value, string info, object args)
            {
                if (step == ExecutionStep.EnterState)
                {
                    // 'value' is the state value that we have transitioned FROM :
                    Console.WriteLine("\t{0} -- {1} -> {2}", value, info, this);
                }
            }

            public override string ToString() { return Value.ToString(); }
        }

        // Since 'Device' has no state graph of its own, define one for derived 'Television' :
        [DeviceTransition(From = Status.Unplugged, When = "Plug", Goto = Status.Off)]
        [DeviceTransition(From = Status.Unplugged, When = "Dispose", Goto = Status.Disposed)]
        [DeviceTransition(From = Status.Off, When = "Switch On", Goto = Status.On)]
        [DeviceTransition(From = Status.Off, When = "Unplug", Goto = Status.Unplugged)]
        [DeviceTransition(From = Status.Off, When = "Dispose", Goto = Status.Disposed)]
        [DeviceTransition(From = Status.On, When = "Switch Off", Goto = Status.Off)]
        [DeviceTransition(From = Status.On, When = "Unplug", Goto = Status.Unplugged)]
        [DeviceTransition(From = Status.On, When = "Dispose", Goto = Status.Disposed)]
        public class Television : Device { }

        public static void Run()
        {
            Console.Clear();

            // Create a television state machine instance, and return it, set in some start state :
            var tv = new Television().Start(Status.Unplugged);
            bool done;

            // Holds iff the chosen start state isn't a final state :
            System.Diagnostics.Debug.Assert(tv != null, "The chosen start state is a final state!");

            // Trigger some state transitions with no arguments
            // ('args' is ignored by this state machine's OnChange(...), anyway) :
            done =
                (
                    tv.
                        MoveNext("Plug").
                        MoveNext("Switch On").
                        MoveNext("Switch Off").
                        MoveNext("Switch On").
                        MoveNext("Switch Off").
                        MoveNext("Unplug").
                        MoveNext("Dispose") // MoveNext(...) returns null iff tv.IsFinal == true
                    == null
                );

            Console.WriteLine();
            Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);

            Console.WriteLine();
            Console.WriteLine("Press any key...");
            Console.ReadKey();
        }
    }
}

'HTH


เป็นเรื่องแปลกไหมที่แต่ละอินสแตนซ์ของรัฐมีสำเนากราฟของรัฐเอง
Groo

@Groo: ไม่พวกเขาทำไม่ได้ เฉพาะกรณีของโทรทัศน์ที่สร้างขึ้นโดยใช้ตัวสร้างส่วนตัวพร้อมสตริง null สำหรับชื่อเล่น (ดังนั้นการเรียกวิธีการ 'สร้าง' ที่ได้รับการป้องกัน) จะมีกราฟสถานะเช่นเดียวกับเครื่องรัฐ ส่วนชื่ออื่น ๆ ของโทรทัศน์ (ที่มีชื่อเล่นไม่เป็นโมฆะสำหรับวัตถุประสงค์ทั่วไปและโฆษณาเฉพาะกิจ) จะเป็นเพียง "จุดแก้ไข" (เพื่อพูด) ซึ่งทำหน้าที่เป็นค่าคงที่ของรัฐ (ซึ่งกราฟสถานะของ เครื่องสถานะจริงจะอ้างอิงเป็นจุดยอด) 'HTH
YSharp

ตกลงฉันเข้าใจแล้ว อย่างไรก็ตาม IMHO มันคงจะดีกว่านี้ถ้าคุณใส่รหัสที่จัดการกับการเปลี่ยนแปลงเหล่านี้ ด้วยวิธีนี้มันทำหน้าที่เป็นเพียงตัวอย่างของการใช้ (IMHO) อินเทอร์เฟซที่ไม่ชัดเจนสำหรับห้องสมุดของคุณ ตัวอย่างเช่นวิธีStateChangeแก้ไข ผ่านการสะท้อน? มันจำเป็นจริงๆเหรอ?
Groo

1
@Groo: คำพูดที่ดี ไม่จำเป็นที่จะต้องคำนึงถึงตัวจัดการในตัวอย่างแรกนั้นเนื่องจากมีการเขียนโปรแกรมอย่างแม่นยำและสามารถตรวจสอบ / ผูกแบบคงที่ (ไม่เหมือนกับเมื่อผ่านแอตทริบิวต์ที่กำหนดเอง) ดังนั้นงานนี้ก็เป็นไปตามที่คาดไว้เช่นกัน: private Television(string moniker, Television value) { Handler<Television, TvOperation, DateTime> myHandler = StateChange; // (code omitted) new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = myHandler } }
YSharp

1
ขอบคุณสำหรับความพยายามของคุณ!
Groo

3

ฉันทำเครื่องรัฐทั่วไปนี้ให้เป็นรหัสของจูเลียต มันทำงานได้ยอดเยี่ยมสำหรับฉัน

นี่คือประโยชน์:

  • คุณสามารถสร้างเครื่องสถานะใหม่ในรหัสที่มีสอง enums TStateและTCommand ,
  • เพิ่ม struct TransitionResult<TState>เพื่อให้สามารถควบคุมผลลัพธ์ผลลัพธ์ของ[Try]GetNext()วิธีการ
  • เผยให้เห็นชั้นซ้อนกันStateTransition เพียงผ่านAddTransition(TState, TCommand, TState)ทำให้ง่ายต่อการทำงานกับมัน

รหัส:

public class StateMachine<TState, TCommand>
    where TState : struct, IConvertible, IComparable
    where TCommand : struct, IConvertible, IComparable
{
    protected class StateTransition<TS, TC>
        where TS : struct, IConvertible, IComparable
        where TC : struct, IConvertible, IComparable
    {
        readonly TS CurrentState;
        readonly TC Command;

        public StateTransition(TS currentState, TC command)
        {
            if (!typeof(TS).IsEnum || !typeof(TC).IsEnum)
            {
                throw new ArgumentException("TS,TC must be an enumerated type");
            }

            CurrentState = currentState;
            Command = command;
        }

        public override int GetHashCode()
        {
            return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            StateTransition<TS, TC> other = obj as StateTransition<TS, TC>;
            return other != null
                && this.CurrentState.CompareTo(other.CurrentState) == 0
                && this.Command.CompareTo(other.Command) == 0;
        }
    }

    private Dictionary<StateTransition<TState, TCommand>, TState> transitions;
    public TState CurrentState { get; private set; }

    protected StateMachine(TState initialState)
    {
        if (!typeof(TState).IsEnum || !typeof(TCommand).IsEnum)
        {
            throw new ArgumentException("TState,TCommand must be an enumerated type");
        }

        CurrentState = initialState;
        transitions = new Dictionary<StateTransition<TState, TCommand>, TState>();
    }

    /// <summary>
    /// Defines a new transition inside this state machine
    /// </summary>
    /// <param name="start">source state</param>
    /// <param name="command">transition condition</param>
    /// <param name="end">destination state</param>
    protected void AddTransition(TState start, TCommand command, TState end)
    {
        transitions.Add(new StateTransition<TState, TCommand>(start, command), end);
    }

    public TransitionResult<TState> TryGetNext(TCommand command)
    {
        StateTransition<TState, TCommand> transition = new StateTransition<TState, TCommand>(CurrentState, command);
        TState nextState;
        if (transitions.TryGetValue(transition, out nextState))
            return new TransitionResult<TState>(nextState, true);
        else
            return new TransitionResult<TState>(CurrentState, false);
    }

    public TransitionResult<TState> MoveNext(TCommand command)
    {
        var result = TryGetNext(command);
        if(result.IsValid)
        {
            //changes state
            CurrentState = result.NewState;
        }
        return result;
    }
}

นี่คือประเภทการคืนค่าของวิธี TryGetNext:

public struct TransitionResult<TState>
{
    public TransitionResult(TState newState, bool isValid)
    {
        NewState = newState;
        IsValid = isValid;
    }
    public TState NewState;
    public bool IsValid;
}

วิธีใช้:

นี่คือวิธีที่คุณสามารถสร้าง OnlineDiscountStateMachineจากคลาสทั่วไป:

กำหนด enum OnlineDiscountStateสำหรับสถานะและ enumOnlineDiscountCommandสำหรับคำสั่งของมัน

กำหนดคลาส OnlineDiscountStateMachineได้มาจากคลาสทั่วไปโดยใช้สอง enums เหล่านั้น

สืบทอดตัวสร้างจากbase(OnlineDiscountState.InitialState)เพื่อให้สถานะเริ่มต้นถูกตั้งค่าเป็นOnlineDiscountState.InitialState

ใช้AddTransitionหลาย ๆ ครั้งตามต้องการ

public class OnlineDiscountStateMachine : StateMachine<OnlineDiscountState, OnlineDiscountCommand>
{
    public OnlineDiscountStateMachine() : base(OnlineDiscountState.Disconnected)
    {
        AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Connected);
        AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Error_AuthenticationError);
        AddTransition(OnlineDiscountState.Connected, OnlineDiscountCommand.Submit, OnlineDiscountState.WaitingForResponse);
        AddTransition(OnlineDiscountState.WaitingForResponse, OnlineDiscountCommand.DataReceived, OnlineDiscountState.Disconnected);
    }
}

ใช้เครื่องสถานะที่ได้รับ

    odsm = new OnlineDiscountStateMachine();
    public void Connect()
    {
        var result = odsm.TryGetNext(OnlineDiscountCommand.Connect);

        //is result valid?
        if (!result.IsValid)
            //if this happens you need to add transitions to the state machine
            //in this case result.NewState is the same as before
            Console.WriteLine("cannot navigate from this state using OnlineDiscountCommand.Connect");

        //the transition was successfull
        //show messages for new states
        else if(result.NewState == OnlineDiscountState.Error_AuthenticationError)
            Console.WriteLine("invalid user/pass");
        else if(result.NewState == OnlineDiscountState.Connected)
            Console.WriteLine("Connected");
        else
            Console.WriteLine("not implemented transition result for " + result.NewState);
    }

1

ฉันคิดว่าเครื่องสถานะที่เสนอโดยจูเลียตมีข้อผิดพลาด: วิธีการGetHashCodeสามารถส่งคืนรหัสแฮชเดียวกันสำหรับการเปลี่ยนสองแบบที่ต่างกันตัวอย่างเช่น

สถานะ = ใช้งาน (1), คำสั่ง = หยุดชั่วคราว (2) => HashCode = 17 + 31 + 62 = 110

สถานะ = หยุดชั่วคราว (2), Command = End (1) => HashCode = 17 + 62 + 31 = 110

เพื่อหลีกเลี่ยงข้อผิดพลาดนี้วิธีการควรเป็นดังนี้:

public override int GetHashCode()
   {
            return 17 + 23 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
   }

อเล็กซ์


1
รหัสแฮชไม่จำเป็นต้องส่งคืนหมายเลขเฉพาะสำหรับชุดค่าผสมใด ๆ ที่เป็นไปได้เฉพาะค่าที่แตกต่างและการกระจายที่ดีในช่วงเป้าหมาย (ในกรณีนี้ช่วงคือintค่าที่เป็นไปได้ทั้งหมด) นั่นเป็นสาเหตุที่HashCodeนำมาใช้ควบคู่กับEqualsเสมอ หากรหัสแฮชเหมือนกันจากนั้นวัตถุจะถูกตรวจสอบความถูกต้องที่แน่นอนโดยใช้Equalsวิธีการ
Dmitry Avtonomov

0

FiniteStateMachine เป็น Simple State Machine เขียนในลิงค์ C #

ข้อดีที่ใช้ห้องสมุดของฉัน FiniteStateMachine:

  1. กำหนดคลาส "บริบท" เพื่อนำเสนออินเทอร์เฟซเดียวกับโลกภายนอก
  2. กำหนดคลาสพื้นฐานนามธรรมของรัฐ
  3. เป็นตัวแทนของ "รัฐ" ที่แตกต่างกันของเครื่องรัฐเป็นคลาสที่ได้รับจากชั้นฐานของรัฐ
  4. กำหนดพฤติกรรมเฉพาะของรัฐในคลาสที่ได้รับจากรัฐที่เหมาะสม
  5. รักษาตัวชี้ไปยัง "สถานะ" ปัจจุบันในคลาส "บริบท"
  6. ในการเปลี่ยนสถานะของเครื่องสถานะให้เปลี่ยนตัวชี้ "สถานะ" ปัจจุบัน

ดาวน์โหลด DLL ดาวน์โหลด

ตัวอย่างบน LINQPad:

void Main()
{
            var machine = new SFM.Machine(new StatePaused());
            var output = machine.Command("Input_Start", Command.Start);
            Console.WriteLine(Command.Start.ToString() + "->  State: " + machine.Current);
            Console.WriteLine(output);

            output = machine.Command("Input_Pause", Command.Pause);
            Console.WriteLine(Command.Pause.ToString() + "->  State: " + machine.Current);
            Console.WriteLine(output);
            Console.WriteLine("-------------------------------------------------");
}
    public enum Command
    {
        Start,
        Pause,
    }

    public class StateActive : SFM.State
    {

        public override void Handle(SFM.IContext context)

        {
            //Gestione parametri
            var input = (String)context.Input;
            context.Output = input;

            //Gestione Navigazione
            if ((Command)context.Command == Command.Pause) context.Next = new StatePaused();
            if ((Command)context.Command == Command.Start) context.Next = this;

        }
    }


public class StatePaused : SFM.State
{

     public override void Handle(SFM.IContext context)

     {

         //Gestione parametri
         var input = (String)context.Input;
         context.Output = input;

         //Gestione Navigazione
         if ((Command)context.Command == Command.Start) context.Next = new  StateActive();
         if ((Command)context.Command == Command.Pause) context.Next = this;


     }

 }

1
มันมีใบอนุญาต GNU GPL
Der_Meister

0

ฉันจะแนะนำstate.cs state.csฉันใช้ state.js (รุ่น JavaScript) เป็นการส่วนตัวและฉันมีความสุขมากกับมัน เวอร์ชั่น C # นั้นทำงานในลักษณะเดียวกัน

คุณยกตัวอย่างสถานะ:

        // create the state machine
        var player = new StateMachine<State>( "player" );

        // create some states
        var initial = player.CreatePseudoState( "initial", PseudoStateKind.Initial );
        var operational = player.CreateCompositeState( "operational" );
        ...

คุณยกตัวอย่างช่วงการเปลี่ยนภาพ:

        var t0 = player.CreateTransition( initial, operational );
        player.CreateTransition( history, stopped );
        player.CreateTransition<String>( stopped, running, ( state, command ) => command.Equals( "play" ) );
        player.CreateTransition<String>( active, stopped, ( state, command ) => command.Equals( "stop" ) );

คุณกำหนดการกระทำในสถานะและช่วงการเปลี่ยนภาพ:

    t0.Effect += DisengageHead;
    t0.Effect += StopMotor;

และนั่นคือ (ค่อนข้างมาก) ดูเว็บไซต์สำหรับข้อมูลเพิ่มเติม


0

NuGet มีแพ็คเกจเครื่องที่ได้รับความนิยมอยู่ 2 แพ็คเกจ

Appccelerate.StateMachine (ดาวน์โหลด 13.6K + 3.82K เวอร์ชั่นเก่า (bbv.Common.StateMachine))

StateMachineToolkit (ดาวน์โหลด 1.56K)

Appccelerate lib มีเอกสารที่ดีแต่ไม่รองรับ. NET 4 ดังนั้นฉันเลือก StateMachineToolkit สำหรับโครงการของฉัน


0

ทางเลือกอื่น ๆ ใน repo นี้https://github.com/lingkodsoft/StateBliss ใช้ไวยากรณ์ได้อย่างคล่องแคล่วรองรับทริกเกอร์

    public class BasicTests
    {
        [Fact]
        public void Tests()
        {
            // Arrange
            StateMachineManager.Register(new [] { typeof(BasicTests).Assembly }); //Register at bootstrap of your application, i.e. Startup
            var currentState = AuthenticationState.Unauthenticated;
            var nextState = AuthenticationState.Authenticated;
            var data = new Dictionary<string, object>();

            // Act
            var changeInfo = StateMachineManager.Trigger(currentState, nextState, data);

            // Assert
            Assert.True(changeInfo.StateChangedSucceeded);
            Assert.Equal("ChangingHandler1", changeInfo.Data["key1"]);
            Assert.Equal("ChangingHandler2", changeInfo.Data["key2"]);
        }

        //this class gets regitered automatically by calling StateMachineManager.Register
        public class AuthenticationStateDefinition : StateDefinition<AuthenticationState>
        {
            public override void Define(IStateFromBuilder<AuthenticationState> builder)
            {
                builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
                    .Changing(this, a => a.ChangingHandler1)
                    .Changed(this, a => a.ChangedHandler1);

                builder.OnEntering(AuthenticationState.Authenticated, this, a => a.OnEnteringHandler1);
                builder.OnEntered(AuthenticationState.Authenticated, this, a => a.OnEnteredHandler1);

                builder.OnExiting(AuthenticationState.Unauthenticated, this, a => a.OnExitingHandler1);
                builder.OnExited(AuthenticationState.Authenticated, this, a => a.OnExitedHandler1);

                builder.OnEditing(AuthenticationState.Authenticated, this, a => a.OnEditingHandler1);
                builder.OnEdited(AuthenticationState.Authenticated, this, a => a.OnEditedHandler1);

                builder.ThrowExceptionWhenDiscontinued = true;
            }

            private void ChangingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
            {
                var data = changeinfo.DataAs<Dictionary<string, object>>();
                data["key1"] = "ChangingHandler1";
            }

            private void OnEnteringHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
            {
                // changeinfo.Continue = false; //this will prevent changing the state
            }

            private void OnEditedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
            {                
            }

            private void OnExitedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
            {                
            }

            private void OnEnteredHandler1(StateChangeInfo<AuthenticationState> changeinfo)
            {                
            }

            private void OnEditingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
            {
            }

            private void OnExitingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
            {
            }

            private void ChangedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
            {
            }
        }

        public class AnotherAuthenticationStateDefinition : StateDefinition<AuthenticationState>
        {
            public override void Define(IStateFromBuilder<AuthenticationState> builder)
            {
                builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
                    .Changing(this, a => a.ChangingHandler2);

            }

            private void ChangingHandler2(StateChangeGuardInfo<AuthenticationState> changeinfo)
            {
                var data = changeinfo.DataAs<Dictionary<string, object>>();
                data["key2"] = "ChangingHandler2";
            }
        }
    }

    public enum AuthenticationState
    {
        Unauthenticated,
        Authenticated
    }
}

0

คุณสามารถใช้โซลูชันของฉันนี่เป็นวิธีที่สะดวกที่สุด ได้ฟรีเช่นกัน

สร้างเครื่องรัฐในสามขั้นตอน:

1.สร้างสกีมใน node editor🔗และโหลดมันในโครงการของคุณโดยใช้library using

StateMachine stateMachine = ใหม่ StateMachine ("โครงการ.xml");

2.อธิบายตรรกะแอปของคุณเกี่ยวกับกิจกรรม⚡

stateMachine.GetState ( "State1") OnExit (Action1).
stateMachine.GetState ( "State2") งาน OnEntry (Action2).
stateMachine.GetTransition ( "Transition1") OnInvoke (Action3).
stateMachine.OnChangeState (Action4);

3.เปิดเครื่องสถานะ🚘

stateMachine.Start ();

ลิงค์:

เครื่องมือแก้ไขโหนด: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

Library: https://github.com/SimpleStateMachine/SimpleStateMachineLibrary

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.