|
bind.hpp |
boost::bind 是標準函數 std::bind1st 和 std::bind2nd 的泛化。它支持任意的函數對象,函數,函數指針,和成員函數指針,它還能將任何參數綁定為一個特定的值,或者將輸入的參數發送到任意的位置。bind 對函數對像沒有任何要求,特別是,它不需要 result_type,first_argument_type 和 second_argument_type 這樣的標準 typedefs。
給定這些定義:
int f(int a, int b)
{
return a + b;
}
int g(int a, int b, int c)
{
return a + b + c;
}
bind(f, 1, 2) 會產生一個「無元」函數對象,它不需要參數並返回 f(1, 2)。同樣,bind(g, 1, 2, 3)() 等價於 g(1, 2, 3)。
有選擇性地只綁定一部分參數也是有可能的。bind(f, _1, 5)(x) 等價於 f(x, 5),這裡,_1 是一個佔位符參數,它的含義是「用第一個輸入參數取代」。
作為對照,這是用標準庫原始形式表達的同樣操作:
std::bind2nd(std::ptr_fun(f), 5)(x);
bind 同樣覆蓋了 std::bind1st 的功能:
std::bind1st(std::ptr_fun(f), 5)(x); // f(5, x) bind(f, 5, _1)(x); // f(5, x)
bind 能夠處理帶有兩個以上參數的函數,而且它的參數取代機制更為直觀:
bind(f, _2, _1)(x, y); // f(y, x) bind(g, _1, 9, _1)(x); // g(x, 9, x) bind(g, _3, _3, _3)(x, y, z); // g(z, z, z) bind(g, _1, _1, _1)(x, y, z); // g(x, x, x)
注意,最後一個示例中,由 bind(g, _1, _1, _1) 生成的函數對像不包含對第一個參數以外的任何參數的引用,但是它仍然能使用一個以上的參數。所有多餘的參數被悄悄地忽略,就像在第三個示例中,第一和第二個參數被忽略。
bind 持有的參數被返回的函數對像拷貝並內部持有。例如,在下面的代碼中:
int i = 5; bind(f, i, _1);
一個 i 的值的拷貝被存儲於函數對像中。boost::ref 和 boost::cref 可用於讓函數對像存儲一個引用而不是拷貝:
int i = 5; bind(f, ref(i), _1); bind(f, cref(42), _1);
bind 並不限於函數,它可以接受任何函數對象。通常情況下,生成的函數對象的 operator() 的返回類型必須顯式指定(沒有 typeof 操作符,返回類型無法推導):
struct F
{
int operator()(int a, int b) { return a - b; }
bool operator()(long a, long b) { return a == b; }
};
F f;
int x = 104;
bind<int>(f, _1, _1)(x); // f(x, x), i.e. zero
有些編譯器遇到 bind<R>(f, ...) 語法會發生問題。出於可移植性的原因,一種和上面的意思相同的可選的表達方式也被支持:
boost::bind(boost::type<int>(), f, _1, _1)(x);
但是要注意,這種可選語法只是作為一個 workaround 提供。它不是接口的一部分。
當函數對像暴露了一個名為 result_type 的內嵌類型時,顯式返回類型可以被省略:
int x = 8; bind(std::less<int>(), _1, 9)(x); // x < 9
【注意:這種省略返回類型的能力並非在所有的編譯器上都可用。】
缺省情況下,bind 為提供的函數對像做出一份拷貝。boost::ref 和 boost::cref 可用於讓它存儲這個函數對象的引用,而非拷貝。當函數對象是不可拷貝的,拷貝代價高昂,或者包含狀態時是很有用的,當然,在這種情況下,要求程序員確保這個函數對像在使用期間不能被銷毀。
struct F2
{
int s;
typedef void result_type;
void operator()( int x ) { s += x; }
};
F2 f2 = { 0 };
int a[] = { 1, 2, 3 };
std::for_each( a, a+3, bind( ref(f2), _1 ) );
assert( f2.s == 6 );
成員函數的指針和數據成員的指針不是函數對象,因為它們不支持 operator()。為了方便起見,bind 接受成員指針作為它的第一個參數,而它的行為就像使用 boost::mem_fn 將成員指針轉換成一個函數對像一樣。換句話說,當 R 是 X::f 的返回類型(作為成員函數)或成員本身的類型(作為數據成員)時,表達式
bind(&X::f, args)
與
bind<R>(mem_fn(&X::f), args)
等價。
【注意:mem_fn 創建的函數對象可以接受一個對象的指針,引用或智能指針作為它的第一個參數,更多的信息,參見 mem_fn 文檔。】
示例:
struct X
{
bool f(int a);
};
X x;
shared_ptr<X> p(new X);
int i = 5;
bind(&X::f, ref(x), _1)(i); // x.f(i)
bind(&X::f, &x, _1)(i); //(&x)->f(i)
bind(&X::f, x, _1)(i); // (internal copy of x).f(i)
bind(&X::f, p, _1)(i); // (internal copy of p)->f(i)
最後兩個示例的有趣之處在於它們生成「自包含」的函數對象。bind(&X::f, x, _1) 存儲 x 的一個拷貝。bind(&X::f, p, _1) 存儲 p 的一個拷貝,而且因為 p 是一個 boost::shared_ptr,這個函數對像保存一個屬於它自己的 X 的實例的引用,而且當 p 離開它的作用域或者被 reset() 之後,這個引用依然保持有效。
傳給 bind 的某些參數可以嵌套 bind 表達式自身:
bind(f, bind(g, _1))(x); // f(g(x))
當函數對像被調用的時候,如果沒有指定順序,內部 bind 表達式先於外部 bind 表達式被求值,在外部 bind 表達式被求值的時候,用內部表達式的求值結果取代它們的佔位符的位置。在上面的示例中,當用參數列表 (x) 調用那個函數對象的時候,bind(g, _1)(x) 首先被求值,生成 g(x),然後 bind(f, g(x))(x) 被求值,生成最終結果 f(g(x))。
bind 的這個特性可被用來執行函數組合。參見示例 bind_as_compose.cpp,示範如何用 bind 達到與 Boost.Compose 類似的功能。
注意第一個參數——被綁定函數對像——是不被求值的,即使它是一個由 bind 生成的函數對像或一個佔位符參數,所以下面的示例不會如你所願地工作:
typedef void (*pf)(int); std::vector<pf> v; std::for_each(v.begin(), v.end(), bind(_1, 5));
你所要的效果,可以通過將一個輔助函數對像 apply 用作它的第一個參數而獲得,作為一個函數對象,它可以支撐它的參數列表。為了方便起見,在 boost/bind/apply.hpp 頭文件中提供了一個 apply 的實現。下面是一個前面的示例的修改版本:
typedef void (*pf)(int); std::vector<pf> v; std::for_each(v.begin(), v.end(), bind(apply<void>(), _1, 5));
儘管在缺省情況下,第一個參數是不被求值的,而所有其它參數被求值。但有時候不需要對第一個參數之後的其它參數求值,甚至當它們是內嵌 bind 子表達式的時候也不需要。這可以由另一個函數對像 protect 來幫助做到,它將類型掩飾起來,讓 bind 無法對它進行識別和求值。在被調用的時候,protect 只是簡單地不加更改地將參數列表轉送到其它函數對像中。
頭文件 boost/bind/protect.hpp 包含一個 protect 的實現。要在求值中保護一個 bind 函數對象,使用 protect(bind(f, ...))。
為了方便起見,由 bind 生成的函數對像重載了 logical not(邏輯非)操作符 ! 和關係操作符 ==, !=, <, <=, >, >=, &&, ||。
如果 logical_not 是一個持有一個參數 x 並返回 !x 的函數對象,則 !bind(f, ...) 等價於 bind( logical_not(), bind(f, ...) )。
如果 op 是一個關係或邏輯操作符,並且 relation 是一個持有兩個參數 a 和 b 並返回 a op b 的函數對象,則 bind(f, ...) op x 等價於 bind( relation(), bind(f, ...), x )。
這實際上意味著你可以方便地對 bind 的結果求非:
std::remove_if( first, last, !bind( &X::visible, _1 ) ); // remove invisible objects
以及方便地將 bind 的結果和一個值進行比較:
std::find_if( first, last, bind( &X::name, _1 ) == "Peter" );
std::find_if( first, last, bind( &X::name, _1 ) == "Peter" || bind( &X::name, _1 ) == "Paul" );
和一個佔位符進行比較:
bind( &X::name, _1 ) == _2
或者和另一個 bind 表達式進行比較:
std::sort( first, last, bind( &X::name, _1 ) < bind( &X::name, _2 ) ); // sort by name
class image;
class animation
{
public:
void advance(int ms);
bool inactive() const;
void render(image & target) const;
};
std::vector<animation> anims;
template<class C, class P> void erase_if(C & c, P pred)
{
c.erase(std::remove_if(c.begin(), c.end(), pred), c.end());
}
void update(int ms)
{
std::for_each(anims.begin(), anims.end(), boost::bind(&animation::advance, _1, ms));
erase_if(anims, boost::mem_fn(&animation::inactive));
}
void render(image & target)
{
std::for_each(anims.begin(), anims.end(), boost::bind(&animation::render, _1, boost::ref(target)));
}
class button
{
public:
boost::function<void()> onClick;
};
class player
{
public:
void play();
void stop();
};
button playButton, stopButton;
player thePlayer;
void connect()
{
playButton.onClick = boost::bind(&player::play, &thePlayer);
stopButton.onClick = boost::bind(&player::stop, &thePlayer);
}
作為一個通用規則,由 bind 生成的函數對像以引用方式持有它們的參數,因此,不能接受非 const 臨時對像或字面常量。這是 C++ 語言的當前(2003)版本與生俱來的局限,稱為 the forwarding problem。(它將在下一版標準(通常稱為 C++0x)中被修正。)
這個庫以下面這種形式的識別標識
template<class T> void f(T & t);
接受任意類型的參數並將它們不加改變地傳遞。注意,這不能用於非 const 右值。
在支持函數模板部分排序的編譯器上,一個可能的解決方案是增加一個重載:
template<class T> void f(T & t); template<class T> void f(T const & t);
很不幸,對於 9 個參數,這樣需要提供 512 個重載,這是不切實際的。這個庫選擇了一個小的子集:對於不大於兩個參數的情況,完整地提供了常量重載,對於三個及更多參數,它為所有參數都以常引用方式持有的情況提供了單一的補充重載。這覆蓋了使用情況的一個合理的部分。
參見專門的 Troubleshooting(故障診斷)部分。
Probably because you used the general bind<R>(f, ...) syntax,
也許是因為你使用了通用的 bind<R>(f, ...) 語法,從而指示 bind 不需要「檢查」 f 以查明參數數量和返回類型的錯誤。
第一個形式指示 bind 去檢查 f 的類型以確定它的 arity(參數數量)和返回類型。參數數量錯誤將在「綁定時間」查明。當然,這樣的語法對 f 有一定的要求。它必須是一個函數,函數指針,成員函數指針,或定義了一個內嵌的名為 result_type 的類型的函數對象,簡而言之,它必須是某種 bind 可以識別的東西。
第二個形式指示 bind 不要試圖識別 f 的類型。它通常和那些沒有或不能暴露 result_type 的函數對像一起使用,但是它也能和非標準函數一起使用。例如,當前實現不能自動識別像 printf 這樣的可變參數函數,所以你必須用 bind<int>(printf, ...)。注意,有一種可選的 bind(type<R>(), f, ...) 語法因為可移植性的原因也被支持。
另一個需要考慮的重要因素是:當 f 是一個函數對像時,不支持模板偏特化或函數模板部分排序的編譯器不能處理第一種形式,而且,大部分情況下,當 f 是一個函數(指針)或成員函數指針時,不能處理第二種形式。
可以,只要你 #define BOOST_BIND_ENABLE_STDCALL。另一個可選方法是將這個函數看成一個一般函數對像並使用 bind<R>(f, ...) 語法。
可以,只要你 #define BOOST_MEM_FN_ENABLE_STDCALL。
可以,只要你 #define BOOST_BIND_ENABLE_PASCAL。另一個可選方法是將這個函數看成一個一般函數對像並使用 bind<R>(f, ...) 語法。
有時可以。在一些平台上,extern "C" 函數指針等價於「一般的」函數指針,所以它們能很好地工作。另一些平台將它們看做不同的類型。期待一個平台相關的 bind 實現顯然可以解決問題,但這個實現不行。照例,workaround 將這個函數看成一個一般函數對像並使用 bind<R>(f, ...) 語法。
一般而言,不可移植擴展在默認狀態下應該關閉,以預防被廠商鎖定。如果適當的宏被自動定義,你可能無意中使用了它而並不確定你的代碼也許不能再移植。另外,有些編譯器有選項讓 __stdcall (__fastcall) 成為它們缺省的調用約定,在這種情況下,就不再需要個別的支持。
在表達式 bind(f, a1, a2, ..., aN) 中,函數對像 f 必須能夠持有正好 N 個參數。這個錯誤通常在「綁定時間」被查出。換句話說,這個編譯錯誤會被報告在 bind() 被調用的那一行:
int f(int, int);
int main()
{
boost::bind(f, 1); // error, f takes two arguments
boost::bind(f, 1, 2); // OK
}
這個錯誤的一個常見變化是忘記成員函數有一個隱含的 "this" 參數:
struct X
{
int f(int);
}
int main()
{
boost::bind(&X::f, 1); // error, X::f takes two arguments
boost::bind(&X::f, _1, 1); // OK
}
和通常的函數調用一樣,被綁定的函數對像必須和參數列表一致。這種不一致通常在「調用時間」被編譯器檢測出來,而結果通常是在 bind.hpp 中類似下面這樣的一行中的錯誤:
return f(a[a1_], a[a2_]);
這種錯誤的一個示例如下:
int f(int);
int main()
{
boost::bind(f, "incompatible"); // OK so far, no call
boost::bind(f, "incompatible")(); // error, "incompatible" is not an int
boost::bind(f, _1); // OK
boost::bind(f, _1)("incompatible"); // error, "incompatible" is not an int
}
佔位符 _N 選擇在「調用時間」被傳遞的參數列表的第 N 個參數。很自然,這是一個試圖訪問超出這個列表的末尾的錯誤:
int f(int);
int main()
{
boost::bind(f, _1); // OK
boost::bind(f, _1)(); // error, there is no argument number 1
}
這個錯誤通常被報告在 bind.hpp 中,類似下面這樣的一行中:
return f(a[a1_]);
一個常見的這類錯誤是在模仿 std::bind1st(f, a) 時,鍵入 bind(f, a, _2),而不是正確的 bind(f, a, _1)。
bind(f, a1, a2, ..., aN) 形式引起對 f 的類型的自動檢測。它不能和任意的函數對像一起工作,f 必須是一個函數或成員函數指針。
定義了 result_type 的函數對像使用這種形式也是允許的,但是只有在編譯器支持偏特化和部分排序的時候才可以。特別是,MSVC 直到版本 7.0 還不支持函數對象的這種語法。
bind<R>(f, a1, a2, ..., aN) 形式支持任意函數對象。
將這種形式用於函數或成員函數指針也是允許的(但不推薦),但是只有在編譯器支持部分排序時才行。特別是,MSVC 直到版本 7.0 還不完全支持函數和成員函數指針的這種語法。
缺省情況下,bind(f, a1, a2, ..., aN) 形式識別「一般的」C++ 函數和函數指針。使用不同的調用約定的函數或像 std::printf 這樣的可變參數函數不能工作。通用的 bind<R>(f, a1, a2, ..., aN) 形式和非標準函數一起工作。
在一些平台上,extern "C" 函數,比如 std::strcmp 不能被短形式的 bind 識別。
綁定一個被重載的函數的企圖通常對導致一個錯誤,因為無法表示到底要綁定哪一個重載版本。對於帶有 const 和非 const 兩個重載的成員函數來說,這是一個很常見的問題,就像這個簡化的示例:
struct X
{
int& get();
int const& get() const;
};
int main()
{
boost::bind( &X::get, _1 );
}
這裡的二義性可以通過將(成員)函數指針強制轉換到想要的類型來解決:
int main()
{
boost::bind( static_cast< int const& (X::*) () const >( &X::get ), _1 );
}
另一個或許更可讀的辦法是引入一個臨時變量:
int main()
{
int const& (X::*get) () const = &X::get;
boost::bind( get, _1 );
}
有些編譯器,包括 MSVC 6.0 和 Borland C++ 5.5.1,處理函數識別標識中的頂層 const 存在問題:
int f(int const);
int main()
{
boost::bind(f, 1); // error
}
workaround:從參數中移除 const 修飾符。
在 MSVC(直到版本 7.0)上,當 boost::bind 是由一個 using 聲明:
using boost::bind;
帶入當前作用域的時候,語法 bind<R>(f, ...) 不能工作。workaround:使用被限定的名字,boost::bind,或者用 using 指令代替:
using namespace boost;
在 MSVC(直到版本 7.0)上,一個內嵌的名為 bind 的類模板會遮蓋函數模板 boost::bind,破壞了 bind<R>(f, ...) 語法。很不幸,某些庫包含內嵌的名為 bind 的類模板(諷刺的是,這些代碼常常是某個 MSVC 特有的 workaround)。
workaround 是使用可選的 bind(type<R>(), f, ...) 語法。
在 MSVC(直到版本 7.0)將可變參數函數(比如 std::printf)中的省略號看作一個類型。因此,它會接受(在當前實現中是錯誤的)形式:
bind(printf, "%s\n", _1);
並會拒絕正確版本:
bind<int>(printf, "%s\n", _1);
namespace boost
{
// no arguments
template<class R, class F> unspecified-1 bind(F f);
template<class F> unspecified-1-1 bind(F f);
template<class R> unspecified-2 bind(R (*f) ());
// one argument
template<class R, class F, class A1> unspecified-3 bind(F f, A1 a1);
template<class F, class A1> unspecified-3-1 bind(F f, A1 a1);
template<class R, class B1, class A1> unspecified-4 bind(R (*f) (B1), A1 a1);
template<class R, class T, class A1> unspecified-5 bind(R (T::*f) (), A1 a1);
template<class R, class T, class A1> unspecified-6 bind(R (T::*f) () const, A1 a1);
template<class R, class T, class A1> unspecified-6-1 bind(R T::*f, A1 a1);
// two arguments
template<class R, class F, class A1, class A2> unspecified-7 bind(F f, A1 a1, A2 a2);
template<class F, class A1, class A2> unspecified-7-1 bind(F f, A1 a1, A2 a2);
template<class R, class B1, class B2, class A1, class A2> unspecified-8 bind(R (*f) (B1, B2), A1 a1, A2 a2);
template<class R, class T, class B1, class A1, class A2> unspecified-9 bind(R (T::*f) (B1), A1 a1, A2 a2);
template<class R, class T, class B1, class A1, class A2> unspecified-10 bind(R (T::*f) (B1) const, A1 a1, A2 a2);
// implementation defined number of additional overloads for more arguments
}
namespace
{
unspecified-placeholder-type-1 _1;
unspecified-placeholder-type-2 _2;
unspecified-placeholder-type-3 _3;
// implementation defined number of additional placeholder definitions
}
所有由 bind 返回的 unspecified-N 類型都是 CopyConstructible(可拷貝構造)的。unspecified-N::result_type 被定義為 unspecified-N::operator() 的返回類型。
所有 unspecified-placeholder-N 都是 CopyConstructible(可拷貝構造)的。它們的拷貝構造函數不會拋出異常。
當 m 是一個非負整數時,函數 μ(x, v1, v2, ..., vm) 被定義為:
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f() 的函數對像 λ,隱式轉換為 R。
拋出:不拋出異常,除非 F 的拷貝構造函數拋出異常。
效果:等價於 bind<typename F::result_type, F>(f);
注意:作為一種擴展,實現允許通過其它手段推斷 f 的返回類型,而不依賴於 result_type 成員。
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f() 的函數對像 λ。
拋出:不拋出異常。
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f(μ(a1, v1, v2, ..., vm)) 的函數對像 λ,隱式轉換為 R。
拋出:不拋出異常,除非 F 或 A1 的拷貝構造函數拋出異常。
效果:等價於 bind<typename F::result_type, F, A1>(f, a1);
注意:作為一種擴展,實現允許通過其它手段推斷 f 的返回類型,而不依賴於 result_type 成員。
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f(μ(a1, v1, v2, ..., vm)) 的函數對像 λ。
拋出:不拋出異常,除非 A1 的拷貝構造函數拋出異常。
效果:等價於 bind<R>(boost::mem_fn(f), a1);
效果:等價於 bind<R>(boost::mem_fn(f), a1);
效果:等價於 bind<R>(boost::mem_fn(f), a1);
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f(μ(a1, v1, v2, ..., vm), μ(a2, v1, v2, ..., vm)) 的函數對像 λ,隱式轉換為 R。
拋出:不拋出異常,除非 F,A1 或 A2 的拷貝構造函數拋出異常。
效果:等價於 bind<typename F::result_type, F, A1, A2>(f, a1, a2);
注意:作為一種擴展,實現允許通過其它手段推斷 f 的返回類型,而不依賴於 result_type 成員。
返回:一個使得表達式 λ(v1, v2, ..., vm) 等價於 f(μ(a1, v1, v2, ..., vm), μ(a2, v1, v2, ..., vm)) 的函數對像 λ。
拋出:不拋出異常,除非 A1 或 A2 的拷貝構造函數拋出異常。
效果:等價於 bind<R>(boost::mem_fn(f), a1, a2);
效果:等價於 bind<R>(boost::mem_fn(f), a1, a2);
實現允許提供補充的 bind 重載以支持更多的參數或不同的函數指針變種。
這個實現支持最多 9 個參數的函數對象。這是一個實現細節,不是設計的固有限制。
有些平台允許通過調用約定(函數被調用時的規則:參數如何傳遞,返回值如何處理,誰來清理棧(如果有的話))來區分(成員)函數的若干類型。
例如,Windows API 函數和 COM 接口成員函數使用一種名為 __stdcall 的調用約定。Borland VCL 組件使用 __fastcall。Mac 工具箱涵數使用一種 pascal 調用約定。
為了和 __stdcall 函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_BIND_ENABLE_STDCALL。
為了和 __stdcall 成員函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_MEM_FN_ENABLE_STDCALL。
為了和 __fastcall 函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_BIND_ENABLE_FASTCALL。
為了和 __fastcall 成員函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_MEM_FN_ENABLE_FASTCALL。
為了和 pascal 函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_BIND_ENABLE_PASCAL。
為了和 __cdecl 成員函數一起使用 bind,在包含 <boost/bind.hpp> 之前 #define 宏 BOOST_MEM_FN_ENABLE_CDECL。
最好在項目選項中定義這些宏,在命令行上使用 –D,或作為使用了 bind 的翻譯單元(.cpp 文件)的第一行。不遵守這個規則,當一個包含了 bind.hpp 的頭文件出現在這些宏被定義之前,可能會導致隱蔽的錯誤。
【注意:這是一個不可移植擴展。它不是接口的一部分。】
【注意:有些編譯器只對 __stdcall 關鍵字提供最小的支持。】
bind 返回的函數對像支持到目前為止尚處於實驗性和未文檔化的 visit_each 枚舉接口。
示例參見 bind_visitor.cpp。
影響這個庫設計的早期成就:
Doug Gregor 建議了一種訪問者機制可以允許 bind 和 signal/slot 庫進行互操作。
John Maddock 修復了一個 MSVC 特有的在 bind 和 type traits 庫之間的衝突。
正式 review 期間,Ross Smith,Richard Crossley,Jens Maurer,Ed Brey 和其他人提出大量改進建議。review 主管者是 Darin Adler。
在和 Jaakko Jarvi 的討論中 bind 的精確語義更加優雅。
John Maddock 修復了一個 MSVC 特有的在 bind 和 iterator adaptors 庫之間的衝突。
Dave Abrahams 改良了 bind 和 mem_fn 以支持在不完善編譯器上的空返回。
Mac Murrett 通過 BOOST_BIND_ENABLE_PASCAL 實現並貢獻了 "pascal" 支持。
可選的 bind(type<R>(), f, ...) 語法的靈感來自於和 Dave Abrahams 與 Joel de Guzman 的一次討論。