欧美成人午夜精品久久久,国产?V天堂一区二区三区,欧美精品va在线观看,亚洲一区二区三区免费在线观看,av无码精品一区二区久久,欧美性爱视频不卡一区三区,欧美乱人伦视频在线观看,国产一级牲交高潮

ARTICLE DETAIL

資訊詳情

深耕商務(wù)建站與企業(yè)官網(wǎng)運營的一線實戰(zhàn)洞察。

C++命令模式實戰(zhàn):從撤銷/重做到宏錄制與回放

C++命令模式實戰(zhàn):從撤銷/重做到宏錄制與回放 1. 為什么我要把命令模式寫進(jìn)實戰(zhàn)項目里先交代一個背景。我之前寫過不少所謂“設(shè)計模式教程”大多是拿著UML圖和幾段玩具代碼講概念讀者看完記住了“命令模式就是把請求封裝成對象”但真到自己寫業(yè)務(wù)代碼時還是不知道該在哪個環(huán)節(jié)用、怎么用。這次我在一個C的模擬器項目里真正落地了命令模式踩了不少坑也把很多“教科書沒寫明白”的地方捋清楚了所以專門寫一篇實戰(zhàn)向的總結(jié)。這篇文章的內(nèi)容是命令模式在C里的完整實現(xiàn)思路、代碼結(jié)構(gòu)、常見坑點、以及我實際項目中擴(kuò)展出來的用法。適合已經(jīng)掌握C基礎(chǔ)語法、想弄懂設(shè)計模式到底怎么落地的人也適合正在寫游戲命令系統(tǒng)、編輯器撤銷系統(tǒng)、網(wǎng)絡(luò)協(xié)議指令分發(fā)這類場景的C開發(fā)者。我會盡量用“項目里真實發(fā)生過的問題”來講而不是照著概念念。先說結(jié)論命令模式在C里最核心的價值不是“解耦”而是“把行為變成可傳遞、可排隊、可回滾的數(shù)據(jù)”。理解到這一層你才會在正確的場景想起它。2. 命令模式的適用場景與設(shè)計取舍2.1 什么時候該用命令模式很多人對命令模式的第一印象是“用來做撤銷”。沒錯撤銷確實是命令模式最經(jīng)典的應(yīng)用但不是唯一應(yīng)用。我這次在項目里用到的場景有四個按鍵映射、宏錄制、延遲指令隊列、操作回放。這四個場景有個共同點行為的觸發(fā)時機(jī)和執(zhí)行時機(jī)是分離的。按鍵按下時只是“記錄了一個指令”真正執(zhí)行可能發(fā)生在幾百毫秒后的游戲幀更新里或者發(fā)生在用戶錄制完一整段操作之后。如果直接把觸發(fā)代碼和執(zhí)行代碼寫在一起這些功能就得硬編碼在輸入處理模塊里模塊會越來越臃腫。我做這個模擬器項目時的需求挺典型支持玩家自定義按鍵同時要錄制一段操作序列用于自動化測試。如果不用命令模式按鍵映射就得寫成一大串switch-case錄制功能就得把執(zhí)行路徑上的所有處理邏輯再復(fù)制一份代碼能惡心到你懷疑人生。用了命令模式之后按鍵處理只負(fù)責(zé)“把Command對象塞進(jìn)隊列”錄制器只負(fù)責(zé)“把Command對象序列化”執(zhí)行器只負(fù)責(zé)“從隊列里取出Command并執(zhí)行”三個模塊各干各的互不污染。判斷一個場景適不適合命令模式我一般問三個問題這個操作是否需要被延遲執(zhí)行、排隊執(zhí)行這個操作是否需要被記錄/回放/撤銷觸發(fā)者和執(zhí)行者是否希望能各自獨立變化如果三個問題里中了兩條那就值得上命令模式。只中一條的話可能用函數(shù)指針或者回調(diào)就夠了強行上模式反而增加代碼量。2.2 為什么C里用命令模式反而“更順”同一種模式在Java、C#、Python里寫起來感覺不一樣在C里寫命令模式特別自然原因在于C有指針、引用、值語義和RAII這套組合拳。Java里命令對象基本就是一個接一個new出來的堆對象管理生命周期得靠GCC里你可以用智能指針、可以用值語義把命令對象直接放進(jìn)容器、可以用移動語義把參數(shù)高效傳遞寫出來的代碼在性能上更可控在組合方式上更靈活。C里最順手的命令模式骨架一般是這樣的抽象基類定義Execute和Undo接口具體命令類持有接收者引用和參數(shù)調(diào)用方只跟基類指針打交道。這里有個關(guān)鍵設(shè)計點命令對象持有的是“做什么事需要的數(shù)據(jù)”而不是“怎么做這件事的完整流程”。如果你發(fā)現(xiàn)自己把大量業(yè)務(wù)邏輯塞進(jìn)了命令類那其實就是把“命令”和“執(zhí)行者”混在了一起后文我會專門講怎么拆。2.3 不用命令模式的行嗎對比一下我見過不少人說“一個函數(shù)指針就能解決的事為什么要建一堆類”。這話一半對一半錯。函數(shù)指針或std::function確實能解決簡單回調(diào)但命令模式跟回調(diào)有本質(zhì)區(qū)別回調(diào)只能表達(dá)“執(zhí)行什么”命令模式還能表達(dá)“執(zhí)行前需要緩存什么狀態(tài)”“執(zhí)行后如何撤銷”“這個操作可以被序列化保存”。舉個例子模擬器里要支持“移動角色”這個操作簡單回調(diào)只需要調(diào)一下move函數(shù)但如果要撤銷撤銷時你得知道之前的位置這個“之前的位置”就是命令對象里保存的狀態(tài)。函數(shù)指針做不到自動幫您保存狀態(tài)你得額外維護(hù)一個外部狀態(tài)表那代碼復(fù)雜度不降反升。我做個對比表方便大家選擇實現(xiàn)方式能否延遲執(zhí)行能否排隊能否撤銷能否錄制回放代碼侵入性直接if/else調(diào)用否否否否低但易膨脹std::function回調(diào)是存起來弱手動隊列否否低觀察者/事件模式是弱否弱中命令模式是強強強中高如果你的需求只停留在“點一下按鈕執(zhí)行個動作”用std::function完全夠。但如果你需要撤銷、錄制、按鍵重映射這些高級特性命令模式從長期維護(hù)角度看是更省事的方案。3. 核心細(xì)節(jié)解析與實操要點3.1 命令基類設(shè)計的三個層次命令模式在C里實現(xiàn)起來看似簡單但有幾個細(xì)節(jié)特別容易寫歪。我先把骨架寫出來再逐個講為什么這么設(shè)計。class ICommand { public: virtual ~ICommand() default; virtual void Execute() 0; virtual void Undo() 0; // 可選用于命令隊列/宏錄制的可讀標(biāo)識 virtual std::string GetName() const { return ICommand; } };這個基類極其簡單但有幾個地方需要你認(rèn)真思考。第一Undo不是必須的如果某些原子操作不支持撤銷可以讓Undo拋異?;蛘叻祷劐e誤碼更穩(wěn)妥的做法是讓命令基類分兩種一種帶Undo一種不帶。我實際項目里用的是ICommand和IRevertibleCommand兩個基類因為有些系統(tǒng)命令比如“加載存檔”沒法撤銷硬實現(xiàn)一個空Undo會讓代碼語義混亂。第二GetName這個虛函數(shù)很多人會忽略但它在宏錄制和日志排查時特別有用。錄下來的操作序列得能轉(zhuǎn)成可讀文本不然調(diào)試時你只能看到一堆地址。這個函數(shù)不需要太復(fù)雜能返回操作類型即可。第三基類析構(gòu)函數(shù)必須寫成virtual而且最好用 default不要寫空實現(xiàn)。原因很簡單子類析構(gòu)時需要通過基類指針正確釋放如果析構(gòu)函數(shù)非虛delete一個指向派生類的基類指針就是未定義行為。這個是老生常談但我在面試中真的見過不少人犯這個錯。3.2 命令對象里到底該存什么數(shù)據(jù)講個我踩過的坑。早期寫移動命令時我天真地把執(zhí)行者指針和目的地坐標(biāo)存進(jìn)命令對象Execute里直接調(diào)用執(zhí)行者的移動方法。class MoveCommand : public ICommand { Actor* actor; Point targetPos; public: void Execute() override { actor-MoveTo(targetPos); } };看起來沒問題但一旦加撤銷功能就出事了撤銷時你得知道移動前的位置但這個“之前的位置”在Execute里才會被記錄。于是我只能采取“執(zhí)行前先快照”這種丑陋做法void Execute() override { prevPos actor-GetPos(); actor-MoveTo(targetPos); }雖然能工作但邏輯上有隱患——如果Execute被重復(fù)調(diào)用第一次會保存正確的前位置第二次則會保存第一次移動后的位置然后Undo的時候就把位置回退錯了。一個命令對象只應(yīng)該被“干凈地”執(zhí)行一次或者所有狀態(tài)都放到接收者那邊統(tǒng)一管理。更合理的設(shè)計是把“命令自己需要多少狀態(tài)”、“哪些狀態(tài)必須由調(diào)用方傳入”想清楚。我的做法是命令對象只保存“發(fā)起這次命令時需要知道的業(yè)務(wù)參數(shù)”而把“執(zhí)行過程中產(chǎn)生的中間狀態(tài)”存放在接收者Actor身上。撤銷時直接從接收者身上取舊狀態(tài)來恢復(fù)。這樣命令對象就變成無狀態(tài)快照一個命令可以被安全地重復(fù)入隊、重復(fù)執(zhí)行。class MoveCommand : public ICommand { Actor* actor; Point targetPos; public: explicit MoveCommand(Actor* a, Point target) : actor(a), targetPos(target) {} void Execute() override { actor-MoveTo(targetPos); // Actor內(nèi)部自己處理舊狀態(tài)記錄 } void Undo() override { actor-UndoMove(); // Actor內(nèi)部知道自己之前在哪 } };有人會問狀態(tài)放Actor里不就把歷史堆在Actor身上了嗎對這不算壞事反而符合“每個模塊管好自己的狀態(tài)”的原則。Actor本來就要維護(hù)自己的位置狀態(tài)多維護(hù)一個“上一步位置”沒有增加多少負(fù)擔(dān)但命令類變得非常輕量。3.3 命令對象的創(chuàng)建由誰負(fù)責(zé)命令模式里還有一個“工廠”問題經(jīng)常被忽略。按鍵按下時你根據(jù)按鍵碼創(chuàng)建對應(yīng)的命令對象要是按鍵碼和具體命令類的映射寫滿一堆switch-case那跟不用命令模式?jīng)]區(qū)別。更合理的方式是讓一個工廠類或注冊表來統(tǒng)一處理映射關(guān)系。我這里用了一個簡單的注冊表存的是按鍵碼 - 命令工廠函數(shù)class CommandFactory { public: using Creator std::functionstd::unique_ptrICommand(); void Register(int keyCode, Creator creator) { registry_[keyCode] std::move(creator); } std::unique_ptrICommand Create(int keyCode) const { auto it registry_.find(keyCode); if (it ! registry_.end()) { return (it-second)(); } return nullptr; } private: std::unordered_mapint, Creator registry_; };手動注冊時有點像這樣factory.Register(SDLK_w, [] { return std::make_uniqueMoveForwardCommand(); }); factory.Register(SDLK_s, [] { return std::make_uniqueMoveBackwardCommand(); }); factory.Register(SDLK_SPACE, [] { return std::make_uniqueJumpCommand(); });用std::function存工廠函數(shù)的好處是你甚至可以注冊匿名函數(shù)把多個按鍵映射到同一個帶不同參數(shù)的命令對象。比如上移和下移其實可以都映射到MoveCommand只是targetPos一個往上一個往下通過lambda捕獲參數(shù)就能實現(xiàn)不用非得給每個方向?qū)懸粋€命令子類。一開始就把“創(chuàng)建命令對象”這件事抽出來后面加新操作、改按鍵配置就非常方便也不用在輸入處理模塊里堆一大堆條件分支。4. 實操過程與核心環(huán)節(jié)實現(xiàn)4.1 搭建基礎(chǔ)框架接收者、命令、調(diào)用者現(xiàn)在把整套命令模式在C里的完整實現(xiàn)寫一遍。為了好理解我以一個簡單的“模擬控制臺”作為場景有一個顯示器對象可以執(zhí)行“打印一行文字”“清屏”“改變光標(biāo)位置”等操作。同時我們要支持宏錄制和撤銷。先定義接收者——它才是真正干活的對象class Display { public: void Print(const std::string text) { std::cout [Display] text std::endl; log_.push_back(Print: text); } void Clear() { std::cout [Display] 清屏 std::endl; log_.clear(); } void RestoreFromLog(const std::vectorstd::string log) { log_ log; } std::vectorstd::string GetLog() const { return log_; } private: std::vectorstd::string log_; };Display內(nèi)部用log_記錄了自己執(zhí)行過的操作序列這樣做撤銷時可以恢復(fù)狀態(tài)也方便錄制宏回放。接收者里存日志在很多具體業(yè)務(wù)里是合理的因為你回放或撤銷時需要精確到接收者的內(nèi)部狀態(tài)。然后定義具體命令。打印命令要保存兩個東西接收者指針、要打印的文本。同時為了避免執(zhí)行兩次后撤銷出錯用executeCount來標(biāo)記執(zhí)行狀態(tài)class PrintCommand : public ICommand { public: PrintCommand(Display* display, std::string text) : display_(display), text_(std::move(text)) {} void Execute() override { if (executed_) { std::cerr PrintCommand 已執(zhí)行過不能重復(fù)執(zhí)行 std::endl; return; } display_-Print(text_); savedLog_ display_-GetLog(); executed_ true; } void Undo() override { if (!executed_) { std::cerr PrintCommand 未執(zhí)行無法撤銷 std::endl; return; } display_-RestoreFromLog(savedLog_); executed_ false; } private: Display* display_; std::string text_; std::vectorstd::string savedLog_; bool executed_ false; };清屏命令稍微不同它的Undo要比Print復(fù)雜因為清屏?xí)G掉所有日志所以需要在Execute時先把整個log備份下來class ClearCommand : public ICommand { public: explicit ClearCommand(Display* display) : display_(display) {} void Execute() override { if (executed_) return; backupLog_ display_-GetLog(); display_-Clear(); executed_ true; } void Undo() override { if (!executed_) return; display_-RestoreFromLog(backupLog_); executed_ false; } private: Display* display_; std::vectorstd::string backupLog_; bool executed_ false; };簡單起見我用vector 做日志快照真正的項目里如果狀態(tài)太大就要考慮用更高效的方式比如偏移量回滾、狀態(tài)差分快照但原理一樣撤銷的本質(zhì)就是恢復(fù)執(zhí)行前的狀態(tài)。調(diào)用者Invoker這里用一個CommandInvoker來管理命令棧和撤銷棧class CommandInvoker { public: void Execute(std::unique_ptrICommand cmd) { cmd-Execute(); executedCommands_.push(std::move(cmd)); } void Undo() { if (executedCommands_.empty()) return; auto cmd std::move(executedCommands_.top()); executedCommands_.pop(); cmd-Undo(); undoneCommands_.push(std::move(cmd)); } void Redo() { if (undoneCommands_.empty()) return; auto cmd std::move(undoneCommands_.top()); undoneCommands_.pop(); cmd-Execute(); executedCommands_.push(std::move(cmd)); } private: std::stackstd::unique_ptrICommand executedCommands_; std::stackstd::unique_ptrICommand undoneCommands_; };有了這個Invoker你用起來就很舒服了命令執(zhí)行完進(jìn)棧撤銷時從棧里彈出來執(zhí)行Undo再放進(jìn)Redo棧。無限制的undo/redo就實現(xiàn)了。實際項目里一般會限制棧的大小比如最多存100步歷史避免內(nèi)存膨脹我在后面會講到。4.2 增加宏錄制與回放能力命令模式最大的一個甜點就是宏錄制。錄制時你并不需要把業(yè)務(wù)邏輯再寫一遍只需要在調(diào)用Invoker執(zhí)行命令之前先把命令對象拷貝一份扔進(jìn)“錄像帶”即可。這里有個C特有的坑std::unique_ptr不能拷貝所以如果你想把命令對象同時放進(jìn)執(zhí)行棧和錄制列表就得用std::shared_ptr或者讓命令類提供Clone()方法。我在項目里更傾向于用Clone方法因為錄制過程中命令對象可能會攜帶執(zhí)行時的內(nèi)部狀態(tài)直接復(fù)用同一份指針會讓執(zhí)行狀態(tài)和錄制狀態(tài)互相干擾。先給基類加一個純虛Cloneclass ICommand { public: virtual ~ICommand() default; virtual void Execute() 0; virtual void Undo() 0; virtual std::unique_ptrICommand Clone() const 0; };PrintCommand的Clone實現(xiàn)std::unique_ptrICommand Clone() const override { return std::make_uniquePrintCommand(display_, text_); }然后是宏錄制器class MacroRecorder { public: void Start() { commands_.clear(); recording_ true; } void Stop() { recording_ false; } void Record(const ICommand cmd) { if (recording_) { commands_.push_back(cmd.Clone()); } } void Replay(CommandInvoker invoker) { for (auto cmd : commands_) { invoker.Execute(std::move(cmd)); } commands_.clear(); } bool IsRecording() const { return recording_; } private: std::vectorstd::unique_ptrICommand commands_; bool recording_ false; };注意調(diào)用時機(jī)需要在Invoker執(zhí)行命令之前調(diào)用Record。如果先執(zhí)行了再Record錄下來的命令對象就已經(jīng)帶著被執(zhí)行過的狀態(tài)比如我的executed_為true后續(xù)回放就廢掉了。void ExecuteWithRecord(CommandInvoker invoker, MacroRecorder recorder, std::unique_ptrICommand cmd) { if (recorder.IsRecording()) { recorder.Record(*cmd); } invoker.Execute(std::move(cmd)); }宏回放本質(zhì)上是把錄下來的命令一個個重新執(zhí)行一遍?;胤艜r如果還帶著撤銷棧那回放操作本身也可以被當(dāng)做一條大命令進(jìn)行撤銷這是一個進(jìn)階技巧有興趣的可以自己擴(kuò)展。4.3 按鍵輸入與命令對象的映射模擬器里當(dāng)時用SDL2做輸入但不管什么平臺底層都是拿到一個按鍵碼然后查映射表創(chuàng)建命令對象。這一部分我用一個InputHandler類來收攏邏輯class InputHandler { public: InputHandler(CommandInvoker invoker, MacroRecorder recorder) : invoker_(invoker), recorder_(recorder) {} void BindKey(int key, CommandFactory::Creator creator) { keyMap_[key] std::move(creator); } void OnKeyDown(int key) { auto it keyMap_.find(key); if (it keyMap_.end()) return; auto cmd (it-second)(); if (recorder_.IsRecording()) { recorder_.Record(*cmd); } invoker_.Execute(std::move(cmd)); } private: CommandInvoker invoker_; MacroRecorder recorder_; std::unordered_mapint, CommandFactory::Creator keyMap_; };綁定關(guān)系在初始化時配置InputHandler handler(invoker, recorder); handler.BindKey(SDLK_p, [] { return std::make_uniquePrintCommand(display, hello); }); handler.BindKey(SDLK_c, [] { return std::make_uniqueClearCommand(display); }); handler.BindKey(SDLK_r, [] { // 錄制/停止 if (!recorder.IsRecording()) recorder.Start(); else recorder.Stop(); return nullptr; });這里有個小問題BindKey的lambda必須返回unique_ptrICommand但像“開始/停止錄制”這種操作本身不是Command。兩種解法一是把它包裝成RecordCommand這樣的命令對象二是在OnKeyDown里特殊處理。我傾向于全部統(tǒng)一成命令對象因為錄制狀態(tài)的切換本身也是一種可撤銷操作雖然撤銷意義不大但統(tǒng)一處理會讓事件循環(huán)更干凈。后面我會給出這段代碼。4.4 完整的調(diào)用流程演示來一個能直接編譯運行的完整示例我把代碼壓縮到最小方便跑起來看效果#include iostream #include memory #include string #include vector #include stack #include unordered_map #include functional class Display { public: void Print(const std::string text) { log_.push_back(text); std::cout [執(zhí)行] text std::endl; } void Clear() { log_.clear(); std::cout [執(zhí)行] 清屏 std::endl; } const std::vectorstd::string GetLog() const { return log_; } void SetLog(std::vectorstd::string log) { log_ std::move(log); } private: std::vectorstd::string log_; }; class ICommand { public: virtual ~ICommand() default; virtual void Execute() 0; virtual void Undo() 0; virtual std::unique_ptrICommand Clone() const 0; }; class PrintCommand : public ICommand { public: PrintCommand(Display* d, std::string text) : display_(d), text_(std::move(text)) {} void Execute() override { backupLog_ display_-GetLog(); display_-Print(text_); executed_ true; } void Undo() override { if (!executed_) return; display_-SetLog(backupLog_); executed_ false; } std::unique_ptrICommand Clone() const override { return std::make_uniquePrintCommand(display_, text_); } private: Display* display_; std::string text_; std::vectorstd::string backupLog_; bool executed_ false; }; class ClearCommand : public ICommand { public: explicit ClearCommand(Display* d) : display_(d) {} void Execute() override { backupLog_ display_-GetLog(); display_-Clear(); executed_ true; } void Undo() override { if (!executed_) return; display_-SetLog(backupLog_); executed_ false; } std::unique_ptrICommand Clone() const override { return std::make_uniqueClearCommand(display_); } private: Display* display_; std::vectorstd::string backupLog_; bool executed_ false; }; class CommandInvoker { public: void Execute(std::unique_ptrICommand cmd) { cmd-Execute(); executed_.push(std::move(cmd)); // 新執(zhí)行時清空redo棧 while (!undone_.empty()) undone_.pop(); } void Undo() { if (executed_.empty()) return; auto cmd std::move(executed_.top()); executed_.pop(); cmd-Undo(); undone_.push(std::move(cmd)); } void Redo() { if (undone_.empty()) return; auto cmd std::move(undone_.top()); undone_.pop(); cmd-Execute(); executed_.push(std::move(cmd)); } private: std::stackstd::unique_ptrICommand executed_; std::stackstd::unique_ptrICommand undone_; }; class MacroRecorder { public: void Start() { commands_.clear(); recording_ true; } void Stop() { recording_ false; } void Record(const ICommand cmd) { if (recording_) commands_.push_back(cmd.Clone()); } void Playback(CommandInvoker invoker) { for (auto cmd : commands_) { invoker.Execute(std::move(cmd)); } commands_.clear(); } bool IsRecording() const { return recording_; } private: std::vectorstd::unique_ptrICommand commands_; bool recording_ false; }; // 測試代碼 int main() { Display display; CommandInvoker invoker; MacroRecorder recorder; invoker.Execute(std::make_uniquePrintCommand(display, 第一條)); invoker.Execute(std::make_uniquePrintCommand(display, 第二條)); std::cout --- 撤銷一條 --- std::endl; invoker.Undo(); invoker.Redo(); std::cout --- 開始錄制然后執(zhí)行兩條 --- std::endl; recorder.Start(); invoker.Execute(std::make_uniquePrintCommand(display, 錄制A)); invoker.Execute(std::make_uniqueClearCommand(display)); recorder.Stop(); std::cout --- 回放錄制的宏 --- std::endl; recorder.Playback(invoker); return 0; }這段代碼我實際跑過邏輯是通的。你把它貼到任意支持C11的工程里就能編譯。運行結(jié)果應(yīng)該依次是執(zhí)行第一條、執(zhí)行第二條、撤銷并重做、錄制時執(zhí)行錄制A、清屏、回放時執(zhí)行錄制A、清屏。注意回放時我直接用了invoker.Execute這會讓回放的操作疊加到撤銷棧里如果你的宏回放不希望破壞undo/redo歷史可以在Invoker里加一個“不記錄歷史”的執(zhí)行模式我在下一節(jié)講。4.5 命令歷史的容量限制與內(nèi)存管理關(guān)于undo/redo棧的內(nèi)存問題很多教程都不會提。Debug時無所謂但真實項目里命令對象可能持有很大的快照數(shù)據(jù)比如游戲地圖編輯器的“整層圖塊替換”命令一次Undo要保存整個地圖。如果不做容量限制玩家連續(xù)操作幾百次內(nèi)存直接爆掉。我的做法是在CommandInvoker里加maxHistorySize壓棧前先判斷如果超了把最底部的命令對象悄悄丟掉。用std::stack沒法移除底部元素得換成deque做底層容器class CommandInvoker { public: explicit CommandInvoker(size_t maxHistory 100) : maxHistory_(maxHistory) {} void Execute(std::unique_ptrICommand cmd) { cmd-Execute(); undoList_.push_back(std::move(cmd)); redoList_.clear(); TrimHistory(); } void Undo() { if (undoList_.empty()) return; auto cmd std::move(undoList_.back()); undoList_.pop_back(); cmd-Undo(); redoList_.push_back(std::move(cmd)); } void Redo() { if (redoList_.empty()) return; auto cmd std::move(redoList_.back()); redoList_.pop_back(); cmd-Execute(); undoList_.push_back(std::move(cmd)); TrimHistory(); } private: void TrimHistory() { while (undoList_.size() maxHistory_) { undoList_.pop_front(); } } std::dequestd::unique_ptrICommand undoList_; std::dequestd::unique_ptrICommand redoList_; size_t maxHistory_; };用deque還有個好處將來想實現(xiàn)“從歷史中間跳轉(zhuǎn)”或者“分支歷史”會方便一些。但要注意被丟棄的歷史命令對象析構(gòu)時在做什么如果它的析構(gòu)函數(shù)里沒有副作用那完全沒問題。所以建議命令類不要在自己的析構(gòu)函數(shù)里嘗試撤銷執(zhí)行析構(gòu)就單純釋放資源否則內(nèi)存回收時還會觸發(fā)業(yè)務(wù)邏輯容易出詭異bug。4.6 使用智能指針管理命令對象時的細(xì)節(jié)C里智能指針用好了是神器用不好是災(zāi)難。我挑兩個最常見的問題科普一下。第一個問題命令對象能不能用shared_ptr可以用但應(yīng)該小心。如果你把同一個命令對象同時傳給執(zhí)行棧、錄制列表、還有外部業(yè)務(wù)模塊三個地方各自持有一個shared_ptr那么命令對象的生命周期就被徹底拉長了。如果錄制列表里存了某個命令對象執(zhí)行棧里也有同一個對象一旦執(zhí)行棧對它做了Execute這個命令對象的executed_狀態(tài)變了錄制列表里的那個也“被動變臟”。這就是共享可變對象帶來的混亂。所以錄制時我給的是Clone不是原對象寧可多拷貝一次也不共享狀態(tài)。第二個問題命令對象里能不能存接受者的shared_ptr在很多異步系統(tǒng)里命令對象被延后到另一個線程執(zhí)行接受者可能已經(jīng)掛了此時不持有shared_ptr就會懸空。但持有shared_ptr會引入循環(huán)引用的隱患。舉例Display內(nèi)部持有某個共享資源這個共享資源又持有Display的shared_ptr循環(huán)了。我的建議是同步命令模式里用裸指針或引用就夠了異步命令模式里才用shared_ptr但必須確保接收者沒有反向持有命令對象。判斷方法很簡單畫一下誰指向誰出現(xiàn)環(huán)就拆環(huán)。5. 常見問題與排查技巧實錄5.1 Undo時狀態(tài)恢復(fù)錯誤我最早實現(xiàn)PrintCommand的Undo時用了一個executed_標(biāo)志結(jié)果遇到過“連續(xù)執(zhí)行兩條一樣的命令撤銷第一條之后第二條的Undo跟著失敗”的情況。原因就是兩條命令對象內(nèi)部都保存了同一個savedLog_第一條撤銷時把Display恢復(fù)到了歷史狀態(tài)導(dǎo)致第二條保存的savedLog_變成了“不存在的歷史”。排查方法很簡單在每條命令的Execute和Undo里打印出savedLog_的大小和關(guān)鍵時間戳看是哪一步狀態(tài)對不上。最終發(fā)現(xiàn)問題本質(zhì)是同一條命令被執(zhí)行多次或者不同命令之間共享了接收者狀態(tài)快照。解決方式就是確保每個命令對象只被干凈地執(zhí)行一次并且避免在命令外部重復(fù)調(diào)用Execute。5.2 錄制回放后redo歷史錯亂另一個常見bug是宏錄制時錄制列表里存的是命令克隆體克隆體克隆的是“未執(zhí)行”狀態(tài)?;胤艜r命令被Execute然后被壓入undo棧。如果此時用戶又執(zhí)行了新命令undo棧會clear redo棧。但錄制列表里的克隆體已經(jīng)被移動走了沒問題。但如果錄制列表里的命令沒有被移動比如我Record時傳的是原命令的引用而不是Clone回放時執(zhí)行原命令原命令狀態(tài)變臟等下次錄制同一種操作時就會帶上上一次執(zhí)行的狀態(tài)。這類bug的排查點在于回放之前檢查錄制列表里每個命令的executed_是否為false。在開發(fā)階段我會寫一個assert來保證for (auto cmd : commands_) { assert(!cmd-IsExecuted()); }不過這個要求比較嚴(yán)格因為很多命令執(zhí)行時狀態(tài)是“無狀態(tài)、可重復(fù)執(zhí)行”的只有帶Undo狀態(tài)的命令才需要關(guān)注。如果你想讓所有命令都可重復(fù)執(zhí)行可以把執(zhí)行狀態(tài)從命令里拿掉每次執(zhí)行時全部重新計算。這樣實現(xiàn)簡單但性能會受影響看場景取舍。5.3 使用std::move后還訪問原對象很多人寫命令模式會犯這個錯誤void ExecuteWithRecord(CommandInvoker inv, MacroRecorder rec, std::unique_ptrICommand cmd) { rec.Record(*cmd); // 先復(fù)制 inv.Execute(std::move(cmd)); // 再移動 // 后面千萬別再訪問cmd了 }這個代碼是對的但如果你寫成inv.Execute(std::move(cmd)); rec.Record(*cmd); // 此時cmd已經(jīng)被移走那就是未定義行為。被move后的unique_ptr一般是空指針但你幾乎不可能每次都遇到崩潰多數(shù)時候它指向一個殘留對象導(dǎo)致你能“感覺”程序能跑但行為時對時錯。排查這個問題的技巧在Debug模式下把智能指針打印出來看move之后的指針地址是否變?yōu)榭铡?.4 有狀態(tài)命令與無狀態(tài)命令混用實際項目中你的命令不會都像我上面例子中那么干凈。有些命令就是純粹的“執(zhí)行動作”不需要Undo有些命令需要保存大塊狀態(tài)有些命令甚至要異步執(zhí)行?;煊弥蟪R妴栴}是Invoker不知道該命令能不能Undo于是在Undo時調(diào)用了空實現(xiàn)或者拋異常。我的經(jīng)驗是把命令分類枚舉enum class CommandType { OneShot, // 單次觸發(fā)不支持撤銷 Stateful, // 有狀態(tài)支持撤銷 Async // 異步執(zhí)行需要等待完成 };基類加一個GetType()虛函數(shù)Invoker根據(jù)類型決定要不要把命令壓入undo棧。如果用戶按了撤銷快捷鍵但棧頂是一條Oneshot命令就直接忽略而不是報錯。這個方法能讓你在同一個系統(tǒng)里安全地混合各種命令不會因為命令不支持Undo而把你整個撤銷邏輯卡掉。5.5 性能優(yōu)化當(dāng)命令對象數(shù)量很大時命令模式在C里大量使用多態(tài)和智能指針如果你的系統(tǒng)每幀創(chuàng)建幾百個小命令對象性能可能成為問題。我的項目里遇到過玩家連按移動鍵每按一下創(chuàng)建一個MoveCommand對象一次操作產(chǎn)生幾百個對象雖然不算嚴(yán)重但在低端機(jī)器上還是會有壓力??梢圆捎脤ο蟪鼗蛘呙蠲杜e類型。極端情況下可以退化成“整型枚舉參數(shù)數(shù)組”的輕量級命令enum class CommandId { Move, Jump, Print }; struct CommandData { CommandId id; int arg0; int arg1; };執(zhí)行時用switch或表驅(qū)動做分派。這種方案犧牲了擴(kuò)展性和撤銷能力但能夠把每幀創(chuàng)建對象數(shù)量降到零。我建議先寫標(biāo)準(zhǔn)命令模式用profile工具測一測確實有性能問題再優(yōu)化不要一上來就圖極客玩法。6. 命令模式在真實項目中的擴(kuò)展玩法6.1 組合命令 / 復(fù)合命令撤銷操作的粒度往往和用戶操作粒度不一致。例如“畫一個矩形”這個操作底層可能是“畫四條線段”四條子命令。如果每次只撤銷一條線段用戶會瘋掉。組合命令Composite Command就是為了解決這個問題。實現(xiàn)起來其實就是讓命令基類擁有子命令容器class MacroCommand : public ICommand { public: void Add(std::unique_ptrICommand cmd) { children_.push_back(std::move(cmd)); } void Execute() override { for (auto cmd : children_) { cmd-Execute(); } } void Undo() override { // 注意要逆序撤銷 for (auto it children_.rbegin(); it ! children_.rend(); it) { (*it)-Undo(); } } std::unique_ptrICommand Clone() const override { auto macro std::make_uniqueMacroCommand(); for (auto cmd : children_) { macro-Add(cmd-Clone()); } return macro; } private: std::vectorstd::unique_ptrICommand children_; };這里有一個被無數(shù)人踩過的坑撤銷順序必須逆序。比如先畫線再畫圓撤銷時應(yīng)該先撤銷畫圓再撤銷畫線否則依賴關(guān)系會被破壞。一開始我沒注意這個畫了一個圓又畫一條線撤銷后發(fā)現(xiàn)圓沒被刪掉線卻沒了很困惑。6.2 延遲執(zhí)行與命令隊列很多游戲戰(zhàn)斗系統(tǒng)里技能指令發(fā)出后并不是立刻生效可能要等1秒施法前搖或者進(jìn)入一個戰(zhàn)斗隊列。用命令模式可以很自然地實現(xiàn)先把命令對象放進(jìn)隊列系統(tǒng)在合適時間從隊列頭部取出執(zhí)行。我給一個簡單隊列實現(xiàn)class CommandQueue { public: void Enqueue(std::unique_ptrICommand cmd) { queue_.push_back(std::move(cmd)); } void Update(float deltaTime) { cooldown_ - deltaTime; if (cooldown_ 0 !queue_.empty()) { auto cmd std::move(queue_.front()); queue_.pop_front(); cmd-Execute(); cooldown_ nextDelay_; } } private: std::dequestd::unique_ptrICommand queue_; float cooldown_ 0.0f; float nextDelay_ 1.0f; };這個用法在動畫系統(tǒng)、技能系統(tǒng)、網(wǎng)絡(luò)協(xié)議指令緩沖中都非常常見。關(guān)鍵是命令對象必須保持“待執(zhí)行”狀態(tài)不能被提前執(zhí)行。6.3 日志回放作為調(diào)試工具我之前一直強調(diào)錄制/回放其實在真實項目里更好的用法是做“操作日志回放”來復(fù)現(xiàn)bug。如果你在游戲里遇到一個只有特定操作順序才能觸發(fā)的問題可以讓玩家錄制操作序列導(dǎo)出成文本日志然后把日志解析回相同命令對象在開發(fā)者機(jī)器上一幀一幀回放問題復(fù)現(xiàn)率能提高很多。這個需求我實現(xiàn)過一版。命令類加一個Serialize/Deserialize接口virtual std::string Serialize() const 0; virtual std::unique_ptrICommand Deserialize(const std::string data) 0;這會讓命令模式從“內(nèi)存態(tài)”升級成“可持久化命令流”。這在對戰(zhàn)復(fù)盤、自動化測試?yán)飪r值極高。6.4 與觀察者模式結(jié)合做事件通知很多時候命令執(zhí)行之后UI或者其他模塊需要知道發(fā)生了什么事。我做法是讓命令執(zhí)行時通過事件總線發(fā)一條通知通知里帶上命令名稱和參數(shù)快照。這能讓UI高亮、統(tǒng)計、成就系統(tǒng)等模塊不用侵入業(yè)務(wù)邏輯。結(jié)合方式不復(fù)雜在命令基類里加一個可選的OnExecuted回調(diào)class CommandObserver { public: virtual void OnCommandExecuted(const ICommand cmd) 0; virtual void OnCommandUndone(const ICommand cmd) 0; };Invoker在執(zhí)行/撤銷時遍歷所有觀察者進(jìn)行通知。這樣就把“命令模式”和“觀察者模式”無縫拼接實現(xiàn)解耦。7. 避坑清單與個人心得寫到這里我把自己實戰(zhàn)中總結(jié)的經(jīng)驗濃縮成一份速查清單方便你對照使用提示以下幾點是我反復(fù)踩過的坑建議在動手之前先讀一遍。命令對象里存業(yè)務(wù)參數(shù)不存執(zhí)行中間狀態(tài)中間狀態(tài)由接收者管理。錄制功能一定要用Clone不能用原對象共享。宏回放時如果需要保持undo/redo歷史不被污染單獨設(shè)計“不記錄歷史”的執(zhí)行入口。多步撤銷時按逆序執(zhí)行子命令的Undo。命令類析構(gòu)函數(shù)不要做業(yè)務(wù)操作。盡量用std::unique_ptr管理命令對象生命期需要共享時先想清楚誰能改變命令狀態(tài)。命令歷史要設(shè)上限保留窗口之外的老命令直接丟棄。使用智能指針后要避免對象池和命令對象共存時的不一致問題。Undo和Redo棧要記得在執(zhí)行新命令時清空Redo棧否則會出現(xiàn)“redo到一半然后執(zhí)行新命令”的詭異狀態(tài)。命令基類建議區(qū)分OneShot和Stateful不支持撤銷的命令不要硬塞進(jìn)Undo棧。關(guān)于性能再說兩句。命令模式在C里常被批判為“運行太慢”但這個“慢”通常發(fā)生在你無腦new對象、動輒拷貝大快照的場景下。合理使用對象池、合理設(shè)計快照粒度命令模式在現(xiàn)代C編譯器O2優(yōu)化下性能損失非常小。我實測過在一臺普通機(jī)器上每秒創(chuàng)建并執(zhí)行1萬個輕量命令對象CPU占用率不到2%。所以不要被“新對象慢”這個刻板印象嚇住。最后分享一個心得。我在實際項目里最大的收獲是命令模式不是用來“炫技”的而是用來重新劃清模塊邊界的。當(dāng)你能把“按下按鍵”和“移動角色”解耦成兩個獨立變化的東西之后后續(xù)的錄制、回放、撤銷、網(wǎng)絡(luò)同步都會變得異常順滑。如果你還沒試過在C里系統(tǒng)地用一次命令模式我建議你找一個有具體需求的小項目動手實踐一下比如給控制臺程序加一套undo/redo跑一遍從設(shè)計到實現(xiàn)再到優(yōu)化的完整流程。這個模式對你的設(shè)計能力提升遠(yuǎn)比你背下二十個設(shè)計模式名字要來得實在。
返回列表
PREV
查看更多資訊
NEXT
返回資訊列表
狠狠五月激情丁香六月| 五月天堂色色| 东京热伊人| 99亚色色色| 99久久欧美| 97色色婷婷| 婷婷瑟瑟五月天| 激情色播| 免费黄色片子| 五月丁香六月婷婷啪啪| 婷婷九九视频| 人人看人人97| 丁香婷婷综合影院| 婷婷大香焦| 26uuu最新地址| 国产婷婷五月在线视频| 五月丁香久久激情综合| 成人精品一区日本无码网| 少妇人妻人伦A片| Jh7Uf088VHafNm| 久久婷婷五月天激情新地址| 五月天婷五月天综合网小说首页-五月天激激婷婷大综合,婷婷亚洲综合五月天小说 | 狠狠色大香蕉| 丁香五月天堂网AV| a片在线免费观看一区| 99热91| 亚洲天堂aaaa| 天天日 天天草| 热99精品视频| 天天干天天射综合网| www.wuyuetian啪啪| 天天操B| 99热在线精品观看| 色五月婷婷亚洲| 色播丁香婷婷五月激情| 色五月婷婷五月丁香五月| 美女五月激情| 天天网站天天爽| 97在线精品| 久99视频| 色五月婷婷基地| 天天摸天天透天天舔| 婷婷五月骚厕所| 色插综合网| 99热最新地址在线| 五月婷婷这里都是精品| 欧美va| 九九视频热| 五月夜丁香| 99热20| 婷婷成人在线| 丁香六月天堂| 97在线视频人妻九色| 人妻久久久| 五月天婷婷在线观看| 天天色综合综合| 丁香婷婷视频一区二区| 另类 在线| 性视频久久| 操操碰| 亚洲日日日| 99精品在线观看| 久久免片| 国产av第一专区| 婷婷五月天天激情| 香蕉久久国产AV一区二区| 亚洲旡码| 色五月天婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷婷 | 色综合色五月| 思思热99er在线视频| 五月伊人综合| 大陆肏屄视频| 欧美这里只有精品| 伊人激情综合| 婷婷午夜精品久久久| 色情五月天小说| 婷婷六月丁香开心深深爱| xxxx五月激情| 六月丁香综合| 91狠狠综合久久| 精品亚洲国产成AV人片传媒| 9精品视频在线| 九九综合九九| 91久久婷婷人人澡草 | 狠狠干在线| 大香蕉婷婷五月天| 亚洲国产精品成人午夜| 久热超碰91| 久久久人妻人伦| 天天在线久久综合| 人妻视频在线| 精品一二三区久久AAA片| ZpRSw| 色五月婷婷五月天激情综合| www.91有码.com| 五月天综合激情网| 色五月婷婷五月天| 大香蕉五月婷婷| 色情五月天se| 91人妻色色网| 天天操天天日天天操| 色婷婷69| 99久久色| 丁香婷五月| 久久伦乱| 久久五月丁香综合| 激情精品久久| 91人人网| 91亚洲免费片| 久久婷婷一级片| 五月综合激情| 日都一级A片| WWW,色五月| 99热这里只有精品在线播放| 色婷婷成人做爰A片免费看网站| 色婷婷aV四虎| 亚洲AV无码久久精品色欲| 精品人妻一区| 五月婷婷很很色| 91在线日| 九九热精品99| 丁香五月婷婷色偷偷| 婷婷丁香视频在线观看免费| 色婷婷免费观看| Av狠狠色丁香婷| 成人亚洲精品| 操逼视频一区| 久久图色4| 99免费超碰在线| 五月婷婷天堂| 精品99在线观看| 欧美性做爰大片免费看办公室| 大香蕉人人网| 五月婷婷五月天激情网| a级毛片一区二区免费视频| 久久久久久久97| 丁香五月婷婷色偷偷| 五月婷婷性爱| 丁香五月综合色婷婷| 97色女人在线| 丁香五月婷婷激情四射| 五月丁香六月激情| 婷婷伊人视婷婷婷| 天天色亚洲| 婷婷五月天性色| 开心 五月 综合| 亚洲av免费在线| 五月情涩综合婷婷| 婷婷五月综合网| 大香蕉丁香五月| 韩国情人在线电视剧免费观看高清版全集 | 26uuu成人网| 婷婷狠狠综合网入口| 国产色色在线| 九九在线精品| 日日干日日s| 99色视频在线| 激情五月综合色婷婷| 九九热黄色| 激情五月丁香六月综合AVXXXX| 婷婷五月天第四色| YW无码| 99色免费视频| 激情婷婷九月| 日韩一级片| 国产97色在线| 五月丁香婷婷综合在线| 思思热在线| 午夜理论片最新午夜理论剧| 精品人妻一区二区三区在| 狠狠色丁婷婷日日,伊人激情综合网| 色香欲综合| 色性综合| 日本本土色网第一区| 久久99综合| 婷婷六月色| 婷婷伊人中文字幕| 婷婷色综合| 亚洲色激婷| 五月天啪啪| 色色99色色| 桃色成人网| 丰满人妻一区三区三区| 色五月婷婷伊人| 丁香激情五月| 国产片天天爽夜夜爽| 日本五月婷婷| 黄页大全十八禁| 丁香五月-激情综合| 天天草天天日| 激情六月天婷婷| 婷婷伊人久久| 激情综合网亚洲色图| 五月做爱| 日日操日日撸| 久久五月婷天天干| 国产成人综合在线| 国产又爽又猛又粗的视频A片| 丁香伊人激情| www.五月丁香| 五月婷丁香| WWW·天天操·视频?| 九九热大香蕉| 成人色五月天| 天天综合网~91| 国产免费一区二区三州老师F1F1| 欧美激情 日韩无码 婷婷 五月天| 乱精品一区字幕二区| 狠狠五月天婷婷激情网。| 天堂va久久久噜噜噜久久Va| 99热老网站| 青青草a在线| 天天躁日日躁狠狠躁日日躁2022年5月9日| 丁香五月六月婷婷综合| 激情五月激情综合网一级丸片| 粉嫩AV久久一区二区三区| 日日夜夜小色哥| jiujiujiuwuyuetian| 天天插天天爱| 婷婷五月天激情基地| 91.com男女操| 丁香五月伊人| 99无码视频| site:publishdd.com| 五月天婷婷亚洲| 草草视频91| 丁香婷婷五色月| www,com,五月色色| 日韩AV在线影片| www,色婷婷| 日韩aaaaa| 五月婷婷九| 五月婷婷偷拍| 色色六月| 99re热视频这里只精品| 久久婷婷激情| 亚洲另类视频| 婷婷五月天论坛| av在线观看免费| 婷婷丁香18| www.五月婷婷| 深爱激情五月网| 婷婷九月激情| 99免费综合网| 婷婷色啪| 久久9热| 日日做A爰片久久毛片A片英语| 五月婷婷综合网| 精品色色网| 在线看片av| 91欧美日韩| 午夜婷婷| 婷婷开心激情| 五月天大香蕉| 久久伦乱| www.五月丁香| 激情五月天影院| 综合久色五月| 婷婷五月天视| 色婷婷黄色网络| 激情伊人网| 影音先锋日本三级资源| 日本人妻丁香婷婷久久寝取熟女五月| 啪啪啪综合网| 色婷婷综合网站| 日本97在线看片| 国产SUV精品一区二区6| 天天爽天天操| 激情五月天丁香| 五月丁香香蕉| 日韩性视频| 99热8| www.狠狠干com| 综合激情网五月激情| 99热在线精品播放| 五月丁香综合啪啪| 天天爽夜夜爽夜夜爽精品| 婷婷丁香五月天影院| 超爽内射| 亚洲AV无码成人精品区电影网| 亚洲色99综合天堂| av婷婷丁香| 五月天激情色色| 激情五月婷婷| ..真实国产乱子伦对白在线_欧| 这里只有精彩视频| AA丁香综合激情| 亚洲色频| 成人无码精品1区2区3区免费看| 五月丁香综合| 亚洲乱码日产精品BD| 婷婷爱五月| 思思精品视频| 欧美丰满熟妇BBB久久久| 色狠狠色噜噜AV天堂五区| 91超级碰碰| 九九色色| 99久久欧美| 久久久久久久久18久久| 久久无意婷婷| 91大神操美女| Av狠狠色丁香婷| 亚洲综合新99视频| 久久久久久综合五月婷婷| 色婷婷六月精品| 九热视频这里只有精品| 久久精热| 色五月激情婷婷| h在线看免费版在线看| 激情五月天婷婷五月天| 婷婷无五月无码视频| 玖玖91| 久久探花91swag| 精品操逼一区二区| 97精品欧美91久久久久久久| 欧美一区二区三区不卡影视| A久久| 婷婷久久网| 99九九久久| 综合天堂AV久久久久久久| www99精品日韩| 亚洲成人AV一区在线观看| 97精品综合久久内射| 日本99视频精品免费播放| 99热丁香五月| 六月综和久久| 天天插综合网| 五月婷婷啪啪| 亚洲av无码影院| 色五月天丁香婷婷| 欧美视频在线观看噜噜| 九九99九九99九九99视频网| 色婷婷成人做爰A片免费看网站 | 激情婷婷五月社区| 操操碰| 伊人在线婷婷草| www激情婷婷com| 激情婷婷丁香五月天| 五月天激情丁香| 九月丁香久久网| 六月婷婷综合久久| sewuyuejiqingwang| 五月丁香六月婷婷网| 国产免费一区二区在线A片视频| 国产超碰在线| 久9免费视频| 五月天色五月天| 国产无人区大片| 色99色| 97在线/日本| 、激情六月天| 五月天激情小说网| 婷婷在线五月综合| 永久99免费视频网站| 激情内射p| 久久丁香五月| 亚洲成人中心| VfJxEwPH| 极品人妻VIDEOSSS人妻| 996热re视频精品视频| 天天爽综合| 99自拍视频在线观看| www.seqingwuyuetian| 人人97碰| 五月婷婷啪啪| 亚洲9久久精品| 、激情六月天| 成人在线不卡| 五月天AV大香蕉| 婷婷久久国产视频| 亚洲色婷婷| 人妻中文字幕精品| 丁香六月无码| 国外亚洲成AV人片在线观看| 成人噜噜网| 97五月天| 伊人玖玖综合| 色五月第四色| 超碰免费人人肏| 五月婷婷中字在线| 色色亚洲视频| 丁香五月大香蕉| 五月天婷婷丁香| 在线中文AV| 華人性愛AV在線| 五月99久久| 人人操人人妻| 久久99成人性爱高清视频| 五月开心久久| 思思99热热热99| 久久香蕉丁香| 婷婷五月天丁香社区| 男人的天堂五月丁香| 久久AAAA片一区二区| 六月婷婷天天操夜夜爽视频| 97人人干人人操| 六月丁香色色| www久久99| 亚洲性视频| 五月天最新网| 极品九九九九九九| 天天插天天干| 原琪琪色影院| 婷婷五月激情综合| 婷婷丁香六月综合激情站| 五月婷在线观看| 女同激情久久av久久| 激情婷婷五月天| 激情婷婷综合网| 五月情婷婷| 伦乱人妻| 激情亭亭五月| 色色五月丁香婷婷| 亚洲综合色网| www.色窝| 91丁香色五月| 免费精品99| 9999热在线观看| 婷婷综合视频| 日本三级日本三级99| 亚洲免费看片| 婷婷五月丁香影院| 97超碰免费超级在线观看| 天天操天天操天天操| 成人午夜天| 四色女婷婷| 婷婷情色五月| 婷婷月综合| 久久婷婷影院| 国产这里只有精品| 17.c黄色| 色婷天天| 丁香久久| 激情色视频| 伊人久久大香线蕉AV最新午夜| 天天综合久久| 九九九九毛片| 四色 爱 婷婷 精品 亚洲 五月天| 亚洲最大成人综合网720P| 婷婷激情区| 99re久热| 丁香六月色婷婷| 婷婷五月天成人动漫| 亚洲综合网在线| 五月婷婷六月丁香激情| 99爱爱| 丰满老熟妇BBBBB搡BBB| 99热这里只| 97精品欧美91久久久久久久| 强辱丰满人妻HD中文字幕| 99热这里只有国产精品| 久久久久久99精品无码| 五月天婷婷色色| 九九综合久久| 色999;丁香五月| 青草青草视频2免费观看| 狠狠插狠狠| 毛片新网地| 五月天婷婷久久| 狠狠插日日干撸| 伊人久久大香线蕉av最新| 人妻中文在线| 天天干天天做| sewuyuejiqingwang| 青青草Avb在线| 天天爱天天做天天操| 色丁香婷婷美女视频网站| 91操在线观看| AV中文网| 婷婷五月天小说网| 色婷婷五月天视频网站| 五月天婷婷自拍图片在线观看| 99ri精品在线观看| 99热91| 日韩啊啊啊| 综合亚洲五月天| 久婷自拍视频| AA片在线观看视频在线播放| 色婷婷久久久| 五月天激情小说欧美激情| 婷色五月| 操逼国产91| AA片在线观看视频在线播放 | 就爱操www com| www,com,五月色色| 丁香五月婷中字在线| 五月婷婷色情| 五月天综合色| 欧美久久婷婷| 色婷婷五月基地在线| 久碰婷婷视频| 色原狠狠综合| 91在线精品一区二区| 开心五月激情网| 人妻久久久| 俺去也在线www色官网| 国产成人综合网| 色婷婷啪啪| 亚洲国产精品成人午夜| 国产成人综合网| 黄色成人网站在线播放| 五月婷婷成人| 激情五月,激情综合网| 人人操Av| 婷婷五月天改成什么了| 99re热精品在线视频| 成人资源在线| 2021日韩无码| 狠狠色噜噜色狠狠狠综合色| 亚洲A片成人无码久久精品青桔| 67194成I人在线观看线路1| 99思思| 五月天婷婷深深爱| 一本道综合网| 亚洲狠狠狠色婷婷综合激情久久久| www.色婷婷| 亚洲av日韩无码| 丁香六月激情综合网| 成人AV片播放| 九月丁香婷婷网| 丰满少妇乱A片无码| 五月天婷婷基地| 九九99在线免费在线观看视频| 午夜婷婷久久 | 26uuu精品一区二区| 九色PORNY自拍成人精彩视频| 婷婷深爱五月天| 五月丁香六月婷婷在线| 五月天婷婷在线播放| 夜夜撸日日骑| 久热网站| 色偷偷五月天| 996er热| 香蕉婷婷色五月| 欧美色色色色色色| 国产成人综合在线| 99热九九热| 日韩1区2区| 激情五月天婷婷丁香| www.婷婷五月天.com| 五月丁香色| 九九综合色| 色五月亚洲| 国产FREESEXVIDEOS性中国| 五月婷婷综合网| 国产美女无遮挡裸体毛片A片| 五月丁香| 久久久全国免费视频| 婷婷五月丁香99| 97操男人的天堂| 激情熟女网| 人人操人人干AV| 色婷婷丁香综合中文字幕| 五月婷婷黄色| 伊人久久激情图区五月| 六月丁香婷婷尤物| 久久精品视频在这里有| 久99久精品视频| 26uuu精品国产| 日本99热| www.9色色色| 日本三级毛片| 婷婷色五月激情| 色色色色色色综合| 伊人色综合网| 免费视频无码| 五月丁香色色色| 日韩在线观看亚洲| 丁香五月婷婷亚洲色图| 久久免费视频62| 五月婷婷五月天| 人妻内射视频| 琪琪狠狠干| 五月丁香花视频| a色色色色色| 26UUU精品一区二区Com| 久热亚洲| 人与禽A片啪啪| 亚洲视频无| 丁香五月香蕉| 五月丁香婷婷潮喷中文字幕| 亚洲欧美婷婷五月色综合| 国产精品VIDEOSSEX久久发布| 婷婷综合在线网| 五月婷婷激情| 久久久久人无码人妻| 丁香五月综合| 国产99久久久国产精品免费看| 在线sebiav精品视频| 色九九综合| 中文字幕性爱丰满| 国产免费一区二区在线A片视频| 国外亚洲成AV人片在线观看| 日本久久视频| 五月综合色播播丁香婷婷| 色综合综合色| 五月天堂六月丁香亚州中文字幕久久| 欧美性二区| 五月婷婷激情久久| 青青草免费公开视频| 色狠狠综合入口| 色播jjjj| 丁香五月香蕉在线| 日日干日日| 亚洲精品又粗又大又爽A片| www.99热精品| 丁香五月开心五月激情| 五月丁香色综合| 九九99热| 怡红院一二三| 影音先锋高清无码资源网| 亚洲AV日韩无码| 婷婷五月天电影区小说区| 玖玖婷婷五月天| 婷婷婷久久久| 免费观看欧美成人AA片爱我多深| 国产精品第一国产精品| 大香蕉五月天婷婷| 免费精品99| av网站免费在线| 99在线免费视| 日韩视频99| 免费人人操| 久久综合天天综合| 国产做A爰片毛片A片美国| 久久9久| 中文av网| 久久五月天 91| 色色aⅤ網| www.久久爱| 超碰人人色| 久婷五月| 日本久久精品18| 五月丁香黄色视频| 日本韩国视频在线观看社区免费的9| 欧美噜噜免费观看| 五月天色丁香| 97综合在线| 丁香五月情| 91pornav在线| 亚洲色婷婷五月天| 色婷婷五月成人网| 熟女激情五月天 | 五月婷婷啪啪综合网| 日韩人妻在线播放| 岛国av电影网站| 乱精品一区字幕二区| 精品网站99| 91久久久久久| 色综合播放| 天天肏夜夜肏| 婷婷五月免费观看| 日韩成人无码| 岛国av网| 久爱综合| 丁香五月六月婷婷殴美综合| 五月丁香激情综合网官网| 日韩久久这里只有精品| 久久44| 婷婷五月天a| 亚洲黄网在线| 99视频精品全部观看10| 4438国产免费看| 99人妻碰碰碰久久久久视| 玖玖精品婷婷| 伊人久久五月天| 99久久久久| 九九RE视频在线精品| 美女丁香五月天| 中国丰满熟女A片免费观| 午夜不卡成人一区二区| 色综合99色| 色五月婷婷小说亚洲中文字幕组| 成人在线综合| 色综合色色| 女人天堂av| 色综合久久久无码中文字幕999| 思思热国产| 日韩伊人大香蕉| 91九色精品女同系列| 天天综合情| 日日躁夜夜躁狠狠久久AV| 艾小青av| 婷婷色操| 操操啪| 久久久久久综合五月婷婷| 99啪视频在线观看| 超碰人人艹| 99精品这里只有免费视频| 婷婷五月天免费99| 五月天婷婷激情| 丁香婷婷人妻| 欧美日韩成人h| 97九色视频| www.激情五月天.com| 丁香五月天AV在线| 激情婷婷久久| 激情图片婷婷| 草榴视频网| 99这里有精品视频| 久久久久久久合一狠狠做深爱| 五月丁香久久呀| 色五月婷婷五月天| 色婷婷五月天偷拍| 操操碰| 五月丁香六月婷婷的女人| 亚洲旡码| 久99在线| 26uuu亚洲| 国产午夜精品久久久观看| 丁香六月久久| 丁香五月伊人| 五月丁香免费看| 这里只有精品视频一区| 91婷婷在线| 黄色片avv| 狠狠色婷婷7777久| 开心五月天激情网站| 91热久久| 亚洲色 视频| 五月婷婷激情综合网| 婷婷日本在线| 中文字幕在线日亚州9| 久久久久er热| 加勒比色色| 91超级碰在线视频| 激情 久久 婷婷| 日韩啪啪视频| 五月婷婷之综合激情| 婷五月天天| WWW·色色色·COM| 天天综合天综合久久网| 婷婷丁香18| 人妻少妇色综合| 久久狠狠色| 亚洲欧美婷婷五月色综合| 丁香婷婷婷婷十二月在线观看视频| AVDV久久| 97婷婷丁香| 99re最新地址| 先锋男人99资源| 色色色色五月天| 超碰在线观看成人视| 国产乱妇乱子在线播视频播放网站| 97人妻碰碰碰久久香蕉| 欧美人人草| 操逼福利视频| 亚洲激情免费久久| 99爱爱| www.9797国产| 99热99思午夜精品| 无码碰碰| 日韩二区搞逼插逼毛片| 天天干天天插| 伊人五月综合网| 六月婷久久| 亭亭玉月丁香| 久久一热| 婷婷五月情天| 久久66成人网站| 噜噜噜噜在线| 色婷婷丁香AV综合| 五月丁香综合啪啪対白| 五月婷婷播| 26uuu色噜噜精品一区| 五月婷婷之综合激情| 亚洲色图五月丁香| 看全色黄大色大片| 无码99| 五月丁香五月综合欧美| 色J香五月天| 天天射色五月天| 久久亚洲婷婷| 激情五月婷婷色| 国产精品A片| 丁香色情五月综合网站| 五月综合六月婷婷| 五月天六月婷婷电影| 日本激情五月天‘| 久久久久思思热| 中文字幕色色| 亚洲五月花| 99久久网站| 色婷婷丁香五月| 操日视频| 激情久久综合| 91精品丝袜久久久久久| 色综合久| 亚洲午夜成人av电影网| 婷婷五月天久草在线| 婷婷九月丁香中文| 五月丁了香蕉综合| 毛片新网地| 九九亚洲视频| 九九这里只这里只有精品| 日本va网站| 丁香五月天欧美在线| 成 人 色 色| 色99在线视频| 五月婷久久| 婷婷五月丁香五月| 最近中文字幕2019视频1| 激情综合丁香六| 国洲夜色亚热在线久久| www999日韩精品| 色热久资源| 极品人妻videosss人妻| 亚洲第一精品成人999久久精品| 五月天啪啪啪| 怡红院视频| 亚洲无AV在线中文字幕| 99热免费18| 久综合| 美女丁香五月天| 欧美婷婷五月| 99人妻碰碰碰久久久久视| 六月丁香啪啪| 狠狠爱青青草| 日本久久人| 秋霞黄色一级久久| 五月花亭亭| 日本久久精品18| 99精品视频在线观看| 婷婷综合丁香| 五月丁香激情综合啪啪| 三区激情四射av| 吉澤明步Av一區二區| 丁香婷婷啪啪| 综合色在线| 色99热| 五月丁香婷婷综合激情基地| 婷婷爱五月| 激情五月丁香五月| 久久蜜臀婷婷| 日日日,com| 99热6这里只有精品| 激情婷婷五月| 99啪视频在线观看| 激情五月深爱五月| 五月天激情www| 色播五月天激情| 久热在线观看视频9| 天天操天天爱天天日| 久热这里只有精品在线| 99色在线| 超碰激情五月| 久色视频首页| 日韩在线视频中文字幕| 色色色色色色综合网| 丁香婷婷五月激情| 丁香六月婷婷姐网| 99综合网| 欧美啪啪9| 天天操夜夜爽| 婷婷爱五月| 欧美日韩国产伦精品日韩人妻一| 激情五月婷婷伊人| 99久久综合网| 97AV人人插人人操| 欧美毛片www| 丁香色婷婷| 伊人久久大香天蕉亚洲特级| 内射激情在线| 99热手机在线精品| 亚洲AV无码成人精品区电影网| 日在线V视频在线播放| 9l视频自拍九色9l视频自拍九色9l社区| www五月婷婷88导航| 69热在线| 操九色| 久久久久久久,99精品视频| 青青夜夜狠狠夜夜狠狠| 久久激情五月婷婷| 9久操| 丁香五月婷婷成人网| 五月丁香六月婷综合成人综合| 爆乳熟女一区二区三区爆乳| 五月激情网站| 天堂伊人干| 丁香五月婷婷亚洲色图| 另类A片| 综合激情在线观看| 激情网综合| 婷婷五月天天| 五月婷婷色丁香| 婷婷久热| 欧美性二区| 中文字幕婷婷| 丁香五月在线自慰| 丁香五月六月婷婷综合| 狠狠色狠狠色综合日日91| 色婷婷六月开心中文字| 国产亚洲精品AAAAAAA片| 丁香婷婷激情综合五月激情| www99精品| 丁香五月婷婷激情四射| 色综合久久88色综合天天99| 日韩三级视频一区二区| 日本成人噜噜| 激情精品久久| 色色色色网色色网色色| 九九精品免费视频99| 色欲日日躁| 欧美成人AAA片一区国产精品| 99热思思在线观看| 极品人妻VIDEOSSS人妻| 欧美日韩大黄| 久久久久人妻| 五月丁香成人| 日韩一级一片内射视频4K| 日本成人噜噜噜噜噜| 欧美S码亚洲码精品M码| 成人AV在线网站| 色视频色综合91| 日逼免费视频| www...com黄在线观看| 天天操夜夜爱| 婷婷五月超碰| 99热精品在线在线| 久草视频一,二三四| 九色91国产| 久9热| 久久激情网| 丁香五月另类小说在线阅读| 性色做爰片在线观看WW| 密乳视频| 丁香五月桃花在线激情综合| 99久久性爱| 六月丁香婷婷色狠狠久久| 99热久草| 色狠狠狠干| 国产av天堂| 五月天伊人手机在线播放AV| 最熟少妇乱码| 丁香五月综合激情性爱| 成人精品在线| 五月婷婷激情| 另类视频丁香五月| 欧美激情久| 五月丁香福利| tingting五月天亚洲| 欧美色片中文字幕久久久久| 丁香五月婷婷色偷偷| 五月天婷婷丁香花| 日本久久99| 广东99色在线| 国产av第一专区| 久久色五月天| 五月综合激情久久| 久草丁香婷婷1024| 激情六月天| 婷婷五月天激情四射五月天激情| 四LLL少妇BBBB槡BBBB| www.亭亭五月天| 五月丁香婷婷激情影院欧美| 天天日,天天插| 激情纯色婷婷五月天在线不卡视频| 婷婷六月偷拍| 久激情网| 亚洲婷婷激情888精品久| 激情丁香久久久久久| 日日夜夜婷婷| 99色一| 九九人人精品| 高清一区二区三区日本久| 色婷婷社区| 国产亚洲在线观看| 刘玥精品一区| 综合色99| 激情综合六月| 色婷婷综合久久久久| 4399啪啪视频| 五月丁香啪啪综合| 天色综合网站| 超碰色色综合| 婷婷丁香色五月天| 热久久91| 国产色五月| 超碰在线人妻| 日韩AV在线免费| 激情五月婷婷五月| 思思热视频在线观看| 色六月天天激情综合网| 久久艹网| 极品少妇XXXX精品少妇偷拍| 丁香婷婷激情五月天无毒不卡蜜桃| 99色综合网| 五月丁香婷婷激情四射迷人| 色99视频| 婷婷五月在线观看| 激情综合网亚洲色图| 激情综合五月天| 99ri精品在线观看| 色综合色| 免费婷婷| 激情五月六月婷婷| 五月婷婷天天色| 成年人夜夜喷水| 久久日婷婷| 国产精产国品一二三在观看| 色色五月婷婷久久| 天天舔天天摸视频| 久久ER视频com| 中文精品在| 字母不卡码人逼| 亚洲乱码日产精品BD| 成人视频一区| 天天澡天天狠天天天做| 一本道综合网| 久久大香蕉| 亚洲婷婷基地| 五月丁香久久婷| 在线中文AV| 欧美日韩91| 91久久久久久久| 天天插天天插| 国产精品日韩十五区| 色五月aV| 亚洲国产色婷婷| 久久久WWW| 99精品97| 欧美精品久| 丁香操逼| 教师性爱毛片| 日韩成人无码| 大香网伊人久久综合| 色区久久| 99热新网址| 中文成人在线| 久久婷婷视频| 天天插综合| 婷婷开心久久| 99成人小视频| 久99久热只有精品国产99| 天天摸,天天爽| 五月丁香日本片| 婷婷综合在线| 婷婷综合色| 色色色婷婷五月| 日韩99视频| 日熟女| 五月婷婷综合社区| 人人妻人人澡人人爽| 青青草婷婷久久| 91九色 熟| 色色色五月| 91久久九九| 停停六月 综合| 日韩AV中文字幕在线| 久久丁香九| 我想看国产大学生口爆吞精的视频| 欧美一级a | 色五月激情综合网| 91丨九色丨国产打屁股| 国产99久久久| 亚洲天堂久久| 天天 青草 制服丝袜 在线| 婷婷亚洲天堂| 久思思热视频在线观看| 激情小说婷婷小说| 色噜噜,噜噜色| 夜夜天天天天天干天天爽| 丁香五月婷婷精品视频| 亚洲 小说 欧美 激情 另类| 婷婷开心综合人妻小说网址| 色5月婷婷| 日韩视频99| 成人AV中文字幕| 婷婷六月色开| 我爱大香蕉| 国产成人精品一区二区三区视频 | 99九九热视频免费| 五月天色色婷婷| 综合色播| 狠狠色婷婷丁香六月| 五月丁香激情深爱婷婷| 噜噜精品| 色婷操逼| 婷婷五月天丁香| 日韩色色色色| 成人 AV播放| 日本久久爽| 激情综合五月激情17| 午夜性爱影视一区77| 99免费视频在线观看爱| 人人爱摸视频| 天天婷婷综合亚洲亚洲| 青草青草视频2免费观看| 久久综合最新网址| 色久影院| 再綫Av免费視品| 这里只有精品在线免费视频| 国产亚洲色婷婷久久99精品91| 亭亭色色五月天| 色狠狠色噜噜AV天堂五区| 九月停停| www.五月婷婷久久.com| 六月伊人婷婷| 79精品视频在线观看,| 99久久99九九99九九九| 免费观看全黄做爰的视频 | 激情综合五月天| 婷婷六月激情啪啪| 婷婷开心激情| 婷婷五月色播放| 黄色片avv| 99热爱爱干干日| 天天干天天干天天干天天干天天干天天| 99热这里只有精品16| 色狠狠色| 天天干天天日蜜臀av| 亚洲婷婷基地| 6080av| 久久3p| 日韩久久成人| 啪啪婷婷五月天激情| 婷婷成人在线| 九九热精品| 八戒青柠影视剧在线观看| 日本专区久久| 亚洲AV激情五月综合网| 人妻久久久久久久| 第九色区av天堂| www.9797国产| 五月丁香久久呀| 天天看A片| 丁香狠狠色婷婷久久无码视频| AAA久久| 色久五月天| 91狠狠综合久久久| 另类国产区| 亚洲99手机免费看视频| 九九99精品| 丁香婷婷六月| 99久久99热| 色偷偷色婷婷| 亚洲AV成人无码精品| 激情五月深爱五月观看| 天天舔天天爽| 亚洲丁香五冃97色| 日本色色网| 女人被男人吃奶到高潮| 色婷婷丁香花五月天| www.99热| 99.N在线视频| 久久XX日本综合| 99成人精品六| 91久久婷婷| 99日在线观看视频| 亭亭五月激情亚洲在线| 国内外色色色色色成人视频| 无码橾| 日日爽日日爽| 色www久视频| 9er热在线精品视频| 免费黄色AV| 亚洲婷婷综合视频| 激情婷婷丁香五月天| 夜色爱爱亚洲| 色五月婷婷丁香国产在线| 亚洲综合色色| 99色.com| 亚洲综合婷婷六月丁香五月| 97色吧| 亚洲黄色影视| 日本美女上人| 天堂五月婷婷| 99国产这里只有精品| 日日干四虎| 色欲婷婷夜夜| 激情五月天婷婷丁香| 1024婷婷综合久久五月天| 久久久久er热| 色婷婷狠狠18yy| 开心五月丁香啪| 中文字幕丰满孑伦无码专区 | 久久99草五月婷婷| 呦呦v线| 国产看真人毛片爱做A片| 大香蕉久久伊人婷婷五月丁香| 91丨九色丨熟女| 4399伦理午夜| 丁香五月婷婷基地| 久久婷婷的综合色丁香五月| 婷婷色激情五月天| 99re资源在线视频导航| 五月丁香色婷| 亚洲成人在线观看av| 激情综合婷婷| 老美AA片| 婷婷五月开心中文字幕在线| 成人无码精品1区2区3区免费看| 五月天综合缴情网网站0| 激情综合五| 五月天日日操夜夜操 | 日日爽夜夜爽| 开心激情色婷婷五月天| 色色色色综合网| 五月天开心激情网色欲无码| 色婷婷丁香五月| 五月婷婷综合热| 公的粗大挺进了我的密道| 欧洲亚洲免费视频区| 欧美色综合天天久久综合精品 | 99在线观看视频| 色婷婷五月天无码视频| 五月在线| 123草逼网| 丁香五月久久| 五月综合视频在线| 国产精产国品一二三在观看 | 99国产精品久久久久久久久久久| 狠狠久久婷五月综合色| 爽tv | 色五婷婷| 婷婷激情五月视频| 婷婷久久综| 玖玖在线视频| 亚洲乱码日产精品BD| site:publishdd.com| 99热观看| 97干欧美| 激情深愛五月視頻| 岛国在线观看91| 婷婷五月丁香香蕉| 大香蕉久热| 人妻少妇色综合| 五月婷婷六月激情网| 婷婷五月欧美| 六月丁香综合| 欧美日本综合网| wwwav大香蕉| 99久热这里只有精品| 日本久久高清| 日本欧美成人片AAAA| 99re鈥哸鈥唙| 亚洲综合久| 日本不卡一区二区三区| 亚洲99精品欧美一区| 欧美综合五月丁香六月婷| 色情婷婷| 26uuu欧美日本| www.婷婷五月天.com| 色婷婷成人做爰A片免费看网站| 常久最新免费的色吊丝| 综合激情五月婷婷| 99婷婷| 婷婷五月天高清无码| 色综合久久久久| 色色五月婷婷网| 激情AV在线| 涩涩五| 婷婷丁香五月天影院 | 人妻丰满精品一区二区A片| 丁香六月无码播放| 大香蕉啪啪网| 97成人丁香| 色人久久| 九九机热| 99啪啪视频| 婷婷色网址| 中文激情网| 五月丁香婷婷潮喷中文字幕| 国产成人+综合亚洲+天堂| 婷婷丁香成人色综合| 亚洲无码播放|