C++11綁定器bind及function機制
前言
之前在學muduo網路庫時,看到陳碩以基於對象編程的方式,大量使用boost
庫中的bind
和function
機制,如今,這些概念都已引入至C++11,包含在頭文件<functional>
中。
本篇文章主要梳理C++綁定器相關的內容以及C++11中引入的function
機制,其中綁定器主要有三種:bind1st
、bind2nd
、bind
(C++11)。學完本篇內容,將對C++綁定器及function
機制等的底層實現有深刻理解,那麼我們開始說吧。
函數對象
首先說說函數對象,之所以說函數對象,是因為綁定器、function
都涉及到該部分概念。函數對象實際上是類調用operator()()
小括弧運算符重載,實現像在「調用函數」一樣的效果,因此還有個別名叫「仿函數」。函數對象示例程式碼如下:
class Print {
public:
void operator()(string &s) { cout << s << endl; }
};
int main() {
string s = "hello world!";
Print print; //定義了一個函數對象print
print(s);
return 0;
}
上面程式碼print(s);
語句,看似像函數調用,其實是類對象print
調用其小括弧運算符重載print.operator(string &s)
。print
就是一個函數對象,至此對函數對象就有了基本的認識。
剖析綁定器bind1st、bind2nd
了解了函數對象,接下來我們說說綁定器,為什麼需要綁定器?在使用STL
時經常會遇到STL
演算法中需要傳遞某元函數對象,比如在寫sort
時,第三個參數決定了我們的排序規則,用來接收一個「比較器」函數對象,該函數對象是一個二元的匿名函數對象,形如greator<int>()
或者less<int>()
。二元函數對象的意思是,這個函數對象的小括弧運算符重載函數接收兩個參數,那麼幾元就表示接收幾個參數。下面是庫中自帶的greater
和less
模板類的源碼實現,可以看到是對小括弧運算符重載的實現,sort
第三個參數接收該模板類的二元匿名函數對象。
template<typename _Tp>
struct greater : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x > __y; }
};
template<typename _Tp>
struct less : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x < __y; }
};
再回到剛才的問題,那為什麼需綁定器?由於STL介面的限制,有時我們拿到的函數對象和特定STL演算法中要接收的函數對象在參數上並不匹配,意思就是需要傳遞一個一元函數對象,你有一個二元函數對象,那可以通過綁定器提前綁定二元函數對象的其中一個參數,使得最終返回的是一個一元函數對象,那麼從二元函數對象到一元函數對象的轉換過程,就需要綁定器去實現。
如STL中的泛型演算法find_if
,可用來查找可變長數組vector
中符合某個條件的值(這個條件比如是要大於50,要小於30,要等於25等等)。其第三個參數需要傳遞一個一元函數對象,假如現在要找到第一個小於70
的數,可將綁定器與二元函數對象結合,轉換為一元函數對象後傳遞給find_if
。
我們知道系統自帶的greater<int>()
和less<int>()
模板類對象是二元匿名函數對象,所以需要通過綁定器將其轉換為一元函數對象,可以通過bind1st
和bind2nd
去綁定,顧名思義,前者對二元函數對象的第一個參數進行綁定,後者對二元函數對象的第二個參數進行綁定,兩個綁定器均返回一元函數對象,用法如下:
sort(vec.begin(), vec.end(), greater<int>()); //從大到小對vector進行排序
find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));
兩個綁定器分別提前綁定了一個參數,使得二元函數對象+綁定器轉換為一元函數對象:
operator()(const T &val)
greater a > b ====> bind1st(greater<int>(), 70) ====> 70 > b
less a < b ====> bind2nd(less<int>(), 70) ====> a < 70
下面給出bind1st
綁定過程圖,二元函數對象綁定了第一個數為70
,變為一元函數對象,傳遞給find_if
泛型演算法,此時find_if
所實現的功能就是:找出有序降序數組中第一個小於70
的數,所以find_if
返回指向65
元素的迭代器:
file:///Users/guochen/Notes/docs/media/16656563650484/16657214749366.jpg
以上就是綁定器的概念。因此需要綁定器的原因就很明顯了,綁定器可以返回一個轉換後的某元函數對象,用於匹配泛型演算法。
根據上面的理解,接下來實現一下bind1st
,程式碼實現如下:
/*可以看到 自己實現的綁定器本質上也是個函數對象 調用operator()進行綁定*/
template<typename Compare, typename T>
class _mybind1st {
public:
_mybind1st(Compare comp, T first) : _comp(comp), _val(first) {}
bool operator()(const T &second) {
return _comp(_val, second);
}
private:
Compare _comp;
T _val;
};
/*實現bind1st 函數模板*/
//直接使用函數模板,好處是可以進行類型推演
template<typename Compare, typename T>
_mybind1st<Compare, T> mybind1st(Compare comp, const T &val) { //綁定器返回值_mybind1st為一元函數對象
return _mybind1st<Compare, T>(comp, val);
}
上述程式碼中mybind1st
綁定器第一個參數Compare comp
是要綁定的二元函數對象,第二個參數val
是在原有函數對象上綁定的值,最後綁定器調用_mybind1st
模板函數對象的小括弧運算符重載並返回該一元匿名函數對象,可以看到_mybind1st
小括弧運算符重載中已將綁定器mybind1st
第二個參數val
傳遞給了原本的二元函數對象Compare comp
,因此原本綁定器接收的二元函數對象只需要處理第二個參數。所以綁定器返回的函數對象_mybind1st
其實是在原本的函數對象上套了一層參數的新的函數對象,閱讀上面的程式碼實現,就可更深刻的理解bind1st
的底層原理。
與此同時,不難寫出bind2nd
的實現,顧名思義該綁定器是對第二個參數進行綁定,不過多贅述,貼出實現程式碼:
template<typename Compare, typename T>
class _mybind2nd {
public:
_mybind2nd(Compare comp, T second) : _comp(comp), _val(second) {}
bool operator()(const T &first) {
return _comp(first, _val);
}
private:
Compare _comp;
T _val;
};
template<typename Compare, typename T>
_mybind2nd<Compare, T> mybind2nd(Compare comp, const T &val) {
return _mybind2nd<Compare, T>(comp, val);
}
根據上文,我們清楚了解到泛型演算法find_if
第三個參數接收一元函數對象,且該泛型演算法功能是尋找第一個符合某條件的元素,我們對其補充實現,程式碼貼出:
/**
* 自己實現了find_if後發現其實綁定器返回的就是綁定後的函數對象
* 使用綁定器的目的:就是將原本某元的函數對象轉化為另一個元的函數對象
* 說白了,綁定器還是對函數對象的一個應用
**/
template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp) {
for(; first != last; ++first) {
if(comp(*first)) { //調用comp的小括弧運算符重載 一元函數對象 comp.operator()(*first)
return first;
}
}
return last;
}
此時要尋找vector
中第一個小於70
的數,就可以這樣寫:
auto it = my_find_if(vec.begin(), vec.end(), mybind1st(greater<int>(), 70));
cout << *it << endl; //列印vec中第一個小於70的數值
以上,圍繞bind1st
、bind2nd
以及函數對象等,展開討論了綁定器bind1st
、bind2nd
的實現原理,但是同時我們也發現其缺點,就是只能對二元函數對象進行綁定轉換,讓其轉換為一元函數對象,那如果遇到很多元的函數對象,我們還得一個一個自己去實現嗎?所以將boost庫的boost::bind
引入到了C++11標準庫中,接下來我們介紹C++11的綁定器std::bind
,它是對上述兩種綁定器的泛化。支援任意函數對象(其實標準庫中最多支援29
元函數對象,不過這也足夠使用了)。
補充:上面都是以函數對象為例,作為綁定器第一個參數傳遞,其實第一個參數可以是函數對象、成員函數、也可以是普通函數。
總結:綁定器本身是函數模板,綁定器第一個參數可能是普通函數、成員函數或函數對象等,返回的一定是函數對象。還有就是這兩個綁定器在C++17
中已移除,因此僅用於學習和理解綁定器,也方便我們對C++11引入的bind
的學習。至於當前這兩個綁定器如何實現對類成員函數的綁定等等我們也沒必要去尋找答案了(我一開始也在努力尋找如何使用這兩個綁定器去綁定類成員函數,但是發現bind
可以很輕鬆地做到,當然如果大家知道怎麼使用bind1st
和bind2nd
綁定類成員函數,也可以評論告知我,感謝~)。
C++11 bind通用綁定器(函數適配器)
我們可將bind
函數看作是一個通用的函數適配器,它接受一個可調用函數對象,生成一個新的可調用函數對象來「適應」原對象的參數列表。bind相比於bind1st和bind2nd,實現了「動態生成新的函數」的功能。簡言之,可通過bind
函數修改原函數並生成一個可以被調用的對象,類似於函數的重載,但是我們又不需要去重新寫一個函數,用bind
函數就可以實現。相信在上面講bind1st和bind2nd時,大家對這些關於綁定器(函數適配器)的概念已經有所認知,我們直接看看如何用的吧。
綁定一個普通函數和函數指針
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int fun(int a, int b, int c, int d, int e) {
return a + b - c + d - e;
}
int main() {
int x = 1, y = 2, z = 3;
auto g = bind(fun, x, y, _2, z, _1); //第一個參數&可省略 但最好寫成&fun
cout << g(11, 22) << endl; // fun(1, 2, 22, 3, 11) => 1+2-22+3-11
// cout << bind(fun, x, y, _2, z, _1)(11, 22) << endl; //等價
}
g
是有兩個參數的二元函數對象,其兩個參數分別用佔位符placeholders::_2
和placeholders::_1
表示,_2
代表二元函數對象的第二個參數22
,_1
代表二元函數對象的第一個參數11
。這個新的可調用對象將它自己的參數作為第三個和第五個傳遞給fun,fun函數的第一個、第二個第四個參數分別被綁定到給定的值x
、y
、z
上。
綁定一個類的靜態成員函數與綁定全局函數沒有任何區別,這裡不做說明,可參考文章:[ 🔗 bind綁定器使用方法 ],該文章中bind詳細用法中描述了對靜態成員方法的使用。
綁定一個類成員函數
綁定器綁定一個成員函數時,我們知道非靜態成員函數第一個參數隱藏了一個this
指針對象,所以綁定時綁定器第二個參數傳遞匿名類對象本身。bind
和之前的bind1st
、bind2nd
一樣,最終返回的一定是函數對象,下面的程式碼將一個五元函數綁定後,返回了一個三元函數對象,效果等同於調用f.operator()(10, 6, 7)
。
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
class Test {
public:
int func(int a, int b, int c, int d, int e) { return a + b - c + d - e; }
};
int main() {
auto f = bind(&Test::func, Test(), _1, 12, _3, 5, _2);
cout << f(10, 6, 7) << endl; //輸出:10+12-7+5-6 = 14
cout << f.operator()(10, 6, 7) << endl;
}
作為類成員函數,需要注意的一點是,如果是非靜態的成員函數,它會存在一個默認的this
指針,靜態的成員函數則不存在this
指針,所以在將其作為bind
函數的參數時,需要注意使用this
指針作為其中一個參數,當使用靜態成員函數作為參數時,其用法和全局函數類似,當參數為類內非靜態成員函數時,第一個參數必須使用&
符號。
註:成為成員函數時,第一個參數之所以必須使用
&
符號,這部分原因可參考:[ 🔗 C++中普通函數指針與類成員函數指針的異同 ],文章中有說明具體原因。
以上就是C++11 bind
的使用方法,衍生於bind1st
、bind2nd
,支援更多的參數綁定,關於bind
函數更多的使用方法,也可參考C++Boost
的說明文檔:[ 📖 Boost.Bind ]。關於bind
函數綁定的過程,可參考:[ 🔗 bind原理圖釋 ],該文章中的圖片方便我們對綁定過程的理解。
C++11 function機制
C++11的function機制是C語言中函數指針的衍生,用來實現回調功能,我們上面的綁定器通常都是以語句執行為單位,當出了某個語句的執行後,綁定器返回的這個函數對象也就隨之消失,因此需要有回調功能的function去長期保留綁定器返回的函數對象,以便在需要的時候隨時通過function機制調用即可。那有人會問,既然有函數指針,為什麼還要再整出來一個function機制?這不是多此一舉嗎?答案肯定是:很有必要,因為function能做到的,函數指針未必能做到,接下來容我花點篇幅去說明為什麼C++中有函數指針還需要std::function
。
為什麼C++中有函數指針還需要std::function?
C/C++中可以使用指針指向一段程式碼,這個指針就叫函數指針,假設有這樣一段程式碼:
#include <stdio.h>
int func(int a) { return a + 1; }
int main() {
int (*f)(int) = func;
printf("%p\n", f);
return 0;
}
我們定義了一個函數func,然後使用指針變數f指向該函數,然後列印出變數f指向的地址,程式碼很簡單,然後我們編譯一下,看下編譯後生成的指令,我們重點關注func函數:
int func(int a) {
4005b6: 55 push %rbp
4005b7: 48 89 e5 mov %rsp,%rbp
4005ba: 89 7d fc mov %edi,-0x4(%rbp)
return a + 1;
4005bd: 8b 45 fc mov -0x4(%rbp),%eax
4005c0: 83 c0 01 add $0x1,%eax
}
4005c3: 5d pop %rbp
4005c4: c3 retq
可以看到,編譯好後的函數func位於地址0x4005b6這個地址,讓我們記住這個地址。然後運行一下編譯後生成的程式,想一想這段程式碼會輸出什麼呢?顯然應該是func函數的在記憶體中的地址!
[root@localhost 07]# ./a.out
0x4005b6
沒有猜錯吧,實際上函數指針本質也是一個指針,只不過這個指針指向的不是記憶體中的一段數據而是記憶體中的一段程式碼,就像這樣:
看到了吧,我們常說的指針一般都是指向記憶體中的一段數據,而函數指針指向了記憶體中的一段程式碼,在這個示例中指向了記憶體地址0x4005b6
,在這個地址中保存了函數func
的機器指令。
現在你應該明白函數指針了,細心的同學可能會有一個疑問,為什麼編譯器在生成可執行文件時就知道函數func
存放在記憶體地址0x4005b6
上呢?這不應該是程式被載入到記憶體後開始運行時才能確定的嗎?
函數指針的作用是可以把一段程式碼當做一個變數傳來傳去,主要的用途之一就是回調函數。關於回調函數其實是在A模組定義,在B模組被調用,就像這樣:
然而有時我們會有這樣的場景,我們依然需要在模組A定義函數,同時函數A的運行需要依賴B模組產生的數據,然後將模組A定義的函數和模組B產生的數據一併傳遞給C模組來調用,就像這樣:
此時,單純的函數指針已經不夠用了,因為函數指針只是單純的指向了記憶體中的一段程式碼,我們不但需要將記憶體中的一段程式碼同時也需要將記憶體中的一塊數據傳遞給模組C,此時你可以定義一個結構體,將程式碼和數據打包起來,就像這樣:
typedef void (*func)(int);
struct functor {
func f;
int arg;
};
我們將這個結構體命名為functor
,注意看,這個結構中有兩部分:
- 一個指向程式碼的指針變數
- 一個保存數據的變數
這樣,我們在A模組為指針變數賦值,在B模組為保存數據的變數賦值,然後將此結構體傳遞給模組C,模組C中可以這樣使用:
void run(struct functor func) {
func->f(func->arg);
}
即,functor既包含了一段程式碼也包含了這段程式碼使用的數據,這裡的數據也被稱為context,即上下文,或者environment,即環境,不管怎麼稱呼,其實就是函數運行依賴的數據:
而這也正是C++中std::function
的目的所在。
單純的函數指針並沒有捕捉上下文的能力,這裡的上下文就是指程式碼依賴的數據,你不得不自己動手構造出一個結構體用來存儲程式碼依賴的上下文。在C++中你沒有辦法單純的利用函數指針指向對象的成員函數,就是因為函數指針沒有辦法捕捉this
(指向對象的指針)這個上下文。
⚠️註:
std::function
的作用本質上和我們剛才定義的結構體區別不大。
利用std::function
你不但可以保存一段程式碼,同時也可以保存必要的上下文,然後在合適的地方基於上下文調用這段程式碼。
根據上文的介紹,我們也知道std::function
相比函數指針的優勢所在,要去理解std::function
,只需要理解上面提到的結構體即可。接下來我們來談談std::function
的用法以及一步一步實現一個簡單的std::function
,剖析其原理。
function的基本用法
接下來直接展示function的直觀用法,我們可以把function想像為一個模板類,調用該模板類的operator()()
小括弧運算符重載,執行封裝的函數指針,關於std::function
具體實現細節,後續再繼續說明,函數指針可用於回調功能,函數對象也可用於回調功能,lambda表達式也可用於回調功能,甚至bind綁定適配後的成員函數也可用於回調功能,那麼在不確定的情況下,通過function機制這樣的泛型機制統一表示,就會很方便。
普通函數:
void hello() {cout << "hello world!" << endl;}
void hello_str(string str) {cout << str << endl;}
int main() {
function<void()> func1 = &hello;
// function<void()> func1(&hello); // 兩種調用方法均可
func1(); //調用func1.operator()() ==> void hello()
function<void(string)> func2 = &hello_str;
func2("hello world"); //調用func2.operator()(string) ==> void hello_str(string)
return 0;
}
模板函數:
template<typename T>
T sum(T a, T b) { return a + b; }
int main() {
function<int(int, int)> func1 = sum<int>;
//調用func1.operator()(int, int) ==> int sum(int, int);
cout << func1(3, 5) << endl; //輸出8
return 0;
}
lambda表達式:
int main() {
function<int(int, int)> func1 = [](int a, int b)->int { return a + b; };
cout << func1(3, 5) << endl; //列印8 調用func1.operator()(int, int) ==> 調用lambda表達式返回求和結果
return 0;
}
函數對象:
class PrintAdd1 {
public:
void operator()(int left, int right) {
cout << "sum : " << left + right << endl;
}
};
int main() {
function<void(int, int)> func1 = PrintAdd1(); //調用默認無參構造函數創建匿名類對象給func1
func1(3, 5); //func1.operator()(int, int) ==> 調用void PrintAdd1.operator(int, int)
return 0;
}
模板函數對象:
template<typename T>
class PrintAdd2 {
public:
void operator()(T left, T right) {
cout << "sum : " << left + right << endl;
}
};
int main() {
function<void(int, int)> func1 = PrintAdd2<int>(); //調用默認無參構造函數創建匿名模板類對象給func1
func1(3, 5); //func1.operator()(int, int) ==> 調用void PrintAdd2.operator()(int, int)
return 0;
}
類靜態成員函數:
class StaticClass1 {
public:
static void hello_static(string s) {
cout << s << endl;
}
};
int main() {
function<void(string)> func1 = &StaticClass1::hello_static;
func1("hello world"); //func1.operator()(string) ==> 調用void hello_static(string)
return 0;
}
模板類靜態成員函數:
template<typename T>
class StaticClass2 {
public:
static void hello_static(T out) {
cout << out << endl;
}
};
int main() {
function<void(string)> func1 = &StaticClass2<string>::hello_static;
func1("static.. hello world"); //func1.operator()(string) ==> 調用void StaticClass2<string>::hello_static(string)
return 0;
}
普通類成員函數:
class Test {
public:
void hello(string str) {
cout << str << endl;
}
};
int main() {
// function<void(Test *, string)> func = &Test::hello;
// func(&Test(), "call Test::hello"); //這種第一個參數傳遞匿名對象的方法在GCC8.4下不可行 在vs2017下可行 不建議使用匿名對象
Test test; //定義對象
function<void(Test *, string)> func1 = &Test::hello;
func1(&test, "call Test::hello"); //func1.operator(Test *, string) ==> 調用void Test::hello(string)
return 0;
}
模板類成員函數:
template<typename T>
class Test2 {
public:
void hello(T str) {
cout << str << endl;
}
};
int main() {
Test2<string> test2;
function<void(Test2<string> *, string)> func1 = &Test2<string>::hello;
func1(&test2, "call template Test::hello"); //func1.operator(Test2 *, string) ==> 調用void Test2<string>::hello(string)
return 0;
}
function底層原理剖析
對function
用法有基本了解後,為了剖析function
底層原理,我們還需知道模板的「特例化」以及「可變參數模板』,這裡不再說明,可參考我以下兩篇博文,已經對模板特化和可變參數模板進行了解釋:
function
是C++11特有的一種比函數指針更靈活的機制,現在如果我們要接收一個hello
函數,形如:
void hello(string str) { cout << str << endl; }
該如何實現呢?function
利用了函數對象的手段,結合函數指針去調用小括弧運算符重載實現,因此理所應當的實現是這樣的,內部有一個函數指針_pfunc
,並且該函數指針在operator()
小括弧運算符重載重被調用:
//前向聲明模板類
template<typename T>
class myfunction {};
//單個類型參數模板完全特例化
template<typename R, typename A1>
class myfunction<R(A1)> {
public:
using PFUNC = R (*)(A1);
public:
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A1 arg) {
return _pfunc(arg);
}
private:
PFUNC _pfunc;
};
當function
對象接收hello
函數時,R
作為返回值會被推導為void
,A1
作為單一參數類型被推導為string
類型:
myfunction<void(string)> f1 = &hello;
f1("hello world"); //列印"hello world"
那現在如果我們要接收兩個參數的sum
求和函數呢?
int sum(int a, int b) { return a + b; }
其實理解了function
的原理後,這個時候要接收一個雙參數sum
求和函數,可以再去特化一個適合sum
的類,其中R
推導為int
,A1
為sum
的第一個參數類型int
,A2
為sum
的第二個類型int
:
#include <iostream>
using namespace std;
int sum(int a, int b) { return a + b; }
template<typename T>
class myfunction {};
//兩個類型參數模板完全特例化
template<typename R, typename A1, typename A2>
class myfunction<R(A1, A2)> {
public:
using PFUNC = R (*)(A1, A2);
public:
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A1 arg1, A2 arg2) {
return _pfunc(arg1, arg2);
}
private:
PFUNC _pfunc;
};
int main() {
myfunction<int(int, int)> f2 = ∑
// myfunction<int(int, int)> f2(&sum);
cout << f2(3, 4) << endl; // 輸出5
return 0;
}
以上就是function
去接收單參數和雙參數時的實現,但是這有個很致命的缺點,如果要接收三個參數、四個參數、十個參數、一百個參數、一千個參數呢?(當然這不太可能,就是單純去考慮這種情況),那是不是還得對不同參數都進行一次實現?那模板的意義何在?如何消除程式碼冗餘就是問題所在,我們需要用到可變模板參數,我的這篇文章已經說明什麼是可變參數模板,如何去使用它:[ 🔗 泛化之美 —— C++11 可變參數模板的妙用 ]
所以通過可變模板參數,我們去實現一個可變參數的function
,該function
可以接收任意數量參數的函數、函數對象、lambda
表達式、bind
適配後的成員函數和普通函數等等:
template<typename T>
class myfunction {};
/*那如果類型參數多了,我們還要一個一個定義嗎??其實可以採用變長參數模板實現*/
// 可變參數模板完全特例化
template<typename R, typename... A>
class myfunction<R(A...)> {
public:
using PFUNC = R (*)(A...);
public:
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A... arg) {
return _pfunc(arg...);
}
private:
PFUNC _pfunc;
};
根據以上實現,我們對function
的底層原理有了深刻的認識,能夠自己實現一個接收任意參數的function
模板類。雖然以上實現和標準庫還差得很遠,但是起到了學習、理解的作用吧。
總結
本文通過C++11之前的bind1st
和bind2nd
引入,介紹了C++11的bind的使用,理清了它的作用,何謂「綁定」?然後介紹了function
的使用方法,並對其進行了邏輯上的實現,以上這些知識點都挺難的,涉及函數對象、模板特化、可變參數模板。但是也算是一次系統性總結吧,感覺意義蠻大的。C++11的路還很遠,以後有機會再繼續深耕吧。
順便記錄一下,我還在為自己的秋招迷茫中,如果有好消息,我在回來還願!