(原:為什麼不能把函數當做一個變數使用?)

我有時候想,一個複雜的遊戲有好多英雄英雄,每個英雄有各種各樣的英雄技能,那如果把英雄技能寫成函數的話,會不會很麻煩?因為需要:

if 當前角色為1

則 火焰衝擊()

elseif 當前角色為2

則 穩固射擊()

等等。那我希望能把函數像數組一樣存儲並編號,比如說0號英雄的技能寫在函數數組的第0個位置,以此類推,寫遊戲時就可以:

技能數組[當前英雄編號].調用()

了。

有沒有這樣的編程語言呢?為什麼不給函數編號?


完全不能這麼做的語言是極少數,如BASIC……

少部分語言可以,但是方法不是很直接,例如C需要定義一個函數指針的數組,C++可以封到std::function裡面,Java需要定義一個interface

其它的多數的語言目前都可以直接把閉包丟數組裡,如Javascript,Python,C#,甚至PHP


絕大多數編程語言根本沒有你說的「為什麼不能把函數當做一個變數」這種問題,這個問題大多數情況下和語言本身沒有什麼關係,單純的是個人技藝達不達標的問題。我們以常見的C語言為例,把一個函數當作一個變數使用在日常編程中簡直再正常不過了。比如:

#include &
#include &

struct FuncTable {
void (*begin)(void);
void (*run)(unsigned int s);
void (*end)(void);
};

void begin_f()
{
printf("Begin
");
}

void run_f(unsigned int s)
{
printf("Run %d seconds
", s);
sleep(s);
}

void end_f()
{
printf("End
");
}

int main(int argc, char *argv[])
{
struct FuncTable ft = {
.begin = begin_f,
.run = run_f,
.end = end_f,
};

ft.begin();
ft.run(3);
ft.end();

return 0;
}

編譯執行:

$ gcc -o mytest mytest.c -Wall
$ ./mytest
Begin
Run 3 seconds
End

再說你描述中的例子,又是在糾結if...else的問題。首先面對多英雄不同技能的這樣一個需求,我們首先想到的是面向對象的編程思想,即總體英雄角色這個大類以及每個角色不同所繼承和派生出的子類以及對象不同。用C++等面向對象的語言我們都知道很容易實現,那要是用不是面向對象語言的C語言呢?

我們舉個簡單的例子,假設所有英雄都可以做移動Move, 普通攻擊Hit,和一個技能SkillA。然後每個不同的英雄可能有不同的技能和攻擊範圍,比如Karma這個英雄有一個SkillB的特別技能,還有一個叫Jax的英雄攻擊都帶範圍攻擊,且他也有一個特別的技能SkillB(區別於Karma的SkillB)。

這樣的需求你怎麼實現?難道要用一堆if...else嗎?

我簡單的實現了下面一個例子,來用C語言模擬一次簡單的面向對象的編程。首先我們要有一個名為Hero的大類,這個大類里有英雄的名字,有一些功能方法,有可供使用的英雄數據。我們簡單的讓每個英雄都能有四個基礎功能——設置移動/攻擊坐標,移動,攻擊,基礎技能攻擊,如下:

#include &
#include &
#include &

/* abstract interface declaration */
struct Hero {
char name[256];
void *funcTable;
void *privateData;
};

struct HeroFuncTable {
void (*SetTarget)(struct Hero * obj, int x, int y);
void (*Move)(struct Hero * obj);
void (*Hit)(struct Hero * obj);
void (*SkillA)(struct Hero * obj);
};

struct HeroPrivateData {
int x, y;
};

void HeroSetTarget(struct Hero * obj, int newx, int newy)
{
struct HeroPrivateData * rdata =
(struct HeroPrivateData*)obj-&>privateData;
rdata-&>x = newx;
rdata-&>y = newy;
}

void HeroMove(struct Hero * obj)
{
struct HeroPrivateData * rdata =
(struct HeroPrivateData*)obj-&>privateData;
printf("%s move to: [%d, %d]
", obj-&>name, rdata-&>x, rdata-&>y);
}

void HeroHit(struct Hero * obj)
{
struct HeroPrivateData * rdata =
(struct HeroPrivateData*)obj-&>privateData;
printf("%s hit target at: [%d, %d]
", obj-&>name, rdata-&>x, rdata-&>y);
}

void HeroSkillA (struct Hero * obj)
{
struct HeroPrivateData * rdata =
(struct HeroPrivateData*)obj-&>privateData;

printf("%s is using skillA on: [%d, %d]
", obj-&>name, rdata-&>x, rdata-&>y);
}

然後我們現在需要一個叫Karma的英雄,他除了具備基本英雄技能以外還有一個自己特有的SkillB技能:

/* Class Karma */
void KarmaSkillB (struct Hero *obj, int arg)
{
struct HeroPrivateData * rdata =
(struct HeroPrivateData*)obj-&>privateData;

printf("Karma is using skillB * %d at: [%d, %d]
", arg, rdata-&>x, rdata-&>y);
}

struct KarmaFuncTable {
void (*SetTarget)(struct Hero * obj, int x, int y);
void (*Move)(struct Hero * obj);
void (*Hit)(struct Hero * obj);
void (*SkillA)(struct Hero * obj);
void (*SkillB)(struct Hero * obj, int arg);
} karmaFuncTable = {
HeroSetTarget,
HeroMove,
HeroHit,
HeroSkillA,
KarmaSkillB
};

struct Hero * MakeKarma (int initx, int inity)
{
struct Hero * obj = malloc (sizeof(struct Hero));
struct HeroPrivateData * rdata = malloc (sizeof(struct HeroPrivateData));
strcpy(obj-&>name, "Karma");
obj-&>funcTable = (struct KarmaFuncTable*) karmaFuncTable;
obj-&>privateData = rdata;

rdata-&>x = initx;
rdata-&>y = inity;

return obj;
}

如上代碼,我們讓Karma繼承所有的Hero的方法和類型,然後在給他提供一個SkillB的特殊技能,最後我們提供一個Karma英雄的構造函數。

下面我們再給出一個Jax英雄,讓Jax英雄擁有範圍攻擊效果,且也有一個特殊技能:

/* Class Jax */
struct JaxPrivateData {
int x, y;
int range;
};

void JaxSetRange(struct Hero * obj, int rg)
{
struct JaxPrivateData * rdata =
(struct JaxPrivateData*)obj-&>privateData;

rdata-&>range = rg;
}

void JaxHit(struct Hero * obj)
{
struct JaxPrivateData * rdata =
(struct JaxPrivateData*)obj-&>privateData;

printf("Jax hit target at: %d[%d, %d]
", rdata-&>range, rdata-&>x, rdata-&>y);
}

void JaxSkillA(struct Hero * obj)
{
struct JaxPrivateData * rdata =
(struct JaxPrivateData*)obj-&>privateData;

printf("Jax is using SkillA at: %d[%d, %d]
", rdata-&>range, rdata-&>x, rdata-&>y);
}

void JaxSkillB(struct Hero * obj)
{
struct JaxPrivateData * rdata =
(struct JaxPrivateData*)obj-&>privateData;

printf("Jax is using SkillB at: %d[%d, %d]
", rdata-&>range, rdata-&>x, rdata-&>y);
}

struct JaxFuncTable {
void (*SetTarget)(struct Hero * obj, int x, int y);
void (*SetRange)(struct Hero * obj, int rg);
void (*Move)(struct Hero * obj);
void (*Hit)(struct Hero * obj);
void (*SkillA)(struct Hero * obj);
void (*SkillB)(struct Hero * obj);
} jaxFuncTable = {
HeroSetTarget,
JaxSetRange,
HeroMove,
JaxHit,
JaxSkillA,
JaxSkillB
};

struct Hero * MakeJax (int initx, int inity, int initr)
{
struct Hero * obj = malloc (sizeof(struct Hero));
struct JaxPrivateData * rdata = malloc (sizeof(struct JaxPrivateData));
strcpy(obj-&>name, "Jax");
obj-&>funcTable = (struct JaxFuncTable*) jaxFuncTable;
obj-&>privateData = rdata;

rdata-&>x = initx;
rdata-&>y = inity;
rdata-&>range = initr;

return obj;
}

如上,我們實現了Jax英雄類,我們讓他繼承了SetTarget和Move的方法,同時因為其特有的範圍攻擊我們給它提供一個額外的range成員變數,並提供了一個SetRange的方法,接著重構了其普通攻擊和技能攻擊的方法,讓這些攻擊都帶範圍效果,然後還實現了一個他特有的SkillB技能,最後我們給出一個Jax英雄的構造函數。

來讓我們測試一下上面的代碼:

/* Do some test */
void KarmaDoSomething(struct Hero *h)
{
struct KarmaFuncTable *f = h-&>funcTable;

f-&>SetTarget(h, 10, 20);
f-&>Move(h);
f-&>Hit(h);
f-&>SkillA(h);
f-&>SkillB(h, 100);
}

void JaxDoSomething(struct Hero *h)
{
struct JaxFuncTable *f = h-&>funcTable;

f-&>SetTarget(h, 50, 88);
f-&>SetRange(h, 8);
f-&>Move(h);
f-&>Hit(h);
f-&>SkillA(h);
f-&>SkillB(h);
}

int main(int argc, char *argv[])
{
struct Hero * heros[2];

heros[0] = MakeKarma(0, 0);
heros[1] = MakeJax(0, 0, 5);

printf("-- Welcome Karma do something for us --
");
KarmaDoSomething(heros[0]);
printf("
");
printf("-- Welcome Jax do something for us --
");
JaxDoSomething(heros[1]);

return 0;
}

如上,我們通過funcTable的類型轉換,實現一個多態的效果,讓Hero去使用它自己的方法集合。我們看一下執行效果:

$ gcc -o hero hero.c -Wall
$ ./hero
-- Welcome Karma do something for us --
Karma move to: [10, 20]
Karma hit target at: [10, 20]
Karma is using skillA on: [10, 20]
Karma is using skillB * 100 at: [10, 20]

-- Welcome Jax do something for us --
Jax move to: [50, 88]
Jax hit target at: 8[50, 88]
Jax is using SkillA at: 8[50, 88]
Jax is using SkillB at: 8[50, 88]

可以看出不同的英雄展現出了不同的動作和技能。

當然這只是我花了二十分鐘寫的一個極其簡單的程序,實際的項目設計肯定要比這個複雜的多,而且也能比我這個寫的好的多。我這裡只是想告訴一些初學者,很多初學者鑽牛角尖的「複雜問題」往往是因為知識還沒有學到位而陷入的自我糾結。我已經碰到過很多人以不同形式問過我類似優化if...else...、優化判斷條件、優化內存結構……等等等等的問題。這些自己以為自己正在面對架構、優化等問題的問題,很多時候根本不是你以為的做架構和優化的樣子。

學習更多的計算機知識,多閱讀優秀的項目代碼,很多時候回看很多問題時才會知道自己當時根本只是維度還不夠高而過早的陷入高維度的糾結而已,包括我自己也是經常這樣。


玩編程,你得記住這麼一條:只要一門語言是圖靈完備的,那麼它當然就能夠做到任何事。

沒錯。圖靈完備=萬能。無非是有時候你需要繞個彎子、有時候又需要額外付出點性能或別的什麼代價而已。


C/C++系:函數名本身就是一個函數指針,可以放進函數數組。你的需求已經被直接滿足。


所有面向對象語言:多態本來就是用來做這個的。你只要讓所有角色都實現同一個介面,使得它們都能提供一個攻擊方法即可(注意每個角色的攻擊方法可以有不同實現,比如火球、暗影箭之類),然後這樣調用就夠了:

foreach 角色 in 角色數組:
角色.攻擊()


面向過程但又不支持函數指針的老奶奶語言(比如BASIC):你需要使用一個叫做dispatch的思路。

//把技能ID和執行這個技能的函數聯繫起來
bool dispatchSpell(spellID id) {
switch(id){
case spellFireball:
return Fireball();
case spellFirebolt:
return Firebolt();
//依此類推,列出其他法術
...
//如果法術id非法,返回false
default:
ASSERT(false); //debug版還是直接崩掉吧
return false;
}
}

//給角色數據結構裡面聲明一個defaultSpell變數:
roleType {
...
spellID defaultSpell;
...
}

//現在,直接循環調用角色數組裡面每個角色的defaultSpell:
foreach role in rolielist:
dispatchSpell(role.defaultSpell);


各種語言的各種「奇技淫巧」,其他答主提到的更多。

總之就一句話,只要語言是圖靈完備的,那麼你需要的各種程序內效果就一定有辦法實現,只是未必那麼自動化而已。

或者說,如果你需要自動語法檢查之類輔助性功能,那麼你或者等編譯器支持,或者就只能通過第三方程序實現;但在程序裡面,你實現的一切功能,只要它是可計算的,那麼就一定有辦法實現。

唯一的問題,就是你自己知不知道該怎麼實現、知不知道常見問題的更優化的解決方案、能否因地制宜的創造出最適合自己所面對的問題的最優化解決方案——你需要冷靜、睿智的通盤考慮問題,千萬別陷在局部細節中。尤其不要只看到那些細節上的好處。它們不僅不解決問題,很多時候反而是攪亂項目的行家裡手。


舉例來說,玩多了面向對象的,很容易直覺的「用繼承來消除if語句」;然後呢,不同角色從角色基類繼承;法師術士都是施法者,都要從「施法者」繼承……如果這樣使得你需要寫if,那麼就說明你需要增加繼承層次,直到所有的if被消除……

很遺憾。但這是胡鬧。

事實上,鼓吹這些的人壓根就沒長程序員腦子。

要稱得上「程序員腦子」,你需要看透問題,不要浮在「法師術士牧師都是施法者」這樣的表面。

事實上,在計算機裡面,「施法」是什麼呢?

施法是這樣一件事:

1、施法往往需要消耗一些資源(魔力、能量、怒氣、體力、hp等)

不管這些資源叫什麼,它就是一個數字,保存於一個變數;消耗資源就是變數減去一個值。

2、施法可能需要某些先決條件(比如被施法者的狀態限制:浮空,特定buf/debuf,和施法者之間的距離等)

3、施法需要角色執行一些動畫(骨骼動畫,典型如unity的avatar)

4、施法需要一些光影效果(手部發光/氣團、面部表情、眼睛發光等)

5、施法會製造一些遊戲物體(拋射體、地面動畫、粒子效果等)

6、施法會有一些影響範圍(單一目標、多目標、傳染等)

7、被法術影響的對象會出現一些狀態改變(扣血、扣藍、加血、加速、減速等等,歸根結底是修改了角色的某個變數所存儲的數值)

……

基本就是這樣。

那麼,一個施法框架應該是這樣:

bool cast(spellID id) {
//根據法術id讀取規則
spellConfig = loadSpellConfig(id);
//執行資源檢定操作
resourceCheck(id);
//判斷先決條件
...
//執行角色動畫
...
//處理光影效果
...
//製造法術實體(火球、暴風雪、子彈乃至刀光劍影,全都可以是法術實體)
...
//計算影響範圍
...
//被擊中目標效果處理(身上帶火、呼叫、亂跑等)
...
//傷害計算
...
}

然後,這些效果可以整理成若干種出來、存入資料庫(所謂的技能數據化);那麼無論你能想出多少個法術、如何千變萬化,這套框架都可以無需修改的支持下來——反正就是那麼幾個效果的排列組合嘛,至多法陣啥的需要換張圖片。那麼,以後無論調什麼,改資料庫就完了,何必動程序代碼呢。

類似的,當你不再把法術釋放的細節和角色綁定之後,你的要求就更容易完成了:

foreach role in roleList {
cast(role.defaultSpell);
}

動用某個技術的目的是讓程序更好寫、更井井有條、更能應對需求變更。

不妨想一想,看看是這個方案更簡單、更清爽、更穩固呢;還是那些「面向對象帶師」們忽悠你的、用海量繼承消除if的方案更不浪費生命。


能放到數組的話,想到3種方法,1是函數指針、2是function包裝、3是重載了operator()的類。下面例子中的的函數隨便寫的,調用的參數也是隨便寫的,只要參數類型和返回類型一致就可以。

  1. 函數指針

#include&
using namespace std;
int 吃飯(int, int) {
return 1;
}
int 睡覺(int, int) {
return 2;
}
int 打豆豆(int, int) {
return 3;
}
int main() {
vector&c{ 吃飯,睡覺,打豆豆 };
c[0](1, 2);//吃飯
c[1](1, 2);//睡覺
c[2](1, 2);//打豆豆
}

2.function包裝

#include&
#include&
using namespace std;
int 吃飯(int, int) {
return 1;
}
int 睡覺(int, int) {
return 2;
}
int 打豆豆(int, int) {
return 3;
}
//差不太多,function類會比函數指針多一些功能
int main() {
vector&< function&&> b{ 吃飯,睡覺,打豆豆 };
b[0](1, 2);//吃飯
b[1](1, 2);//睡覺
b[2](1, 2);//打豆豆
}

3.重載operator()的類。需要寫抽象類,擴展性更高

#include&
using namespace std;
class 活動 {
public:
virtual int operator()(int, int) = 0;
};
class 吃飯 :public 活動{
public:
int operator()(int, int)override {
return 1;
}
};
class 睡覺 :public 活動 {
public:
int operator()(int, int)override {
return 2;
}
};
class 打豆豆 :public 活動 {
public:
int operator()(int, int)override {
return 3;
}
};
int main() {
吃飯 x;
睡覺 y;
打豆豆 z;
vector&c{ x,y,z };
//調用方式和函數指針類似,虛函數本質也就是函數指針
(*c[0])(1, 2);//吃飯
(*c[1])(1, 2);//睡覺
(*c[2])(1, 2);//打豆豆
}

如果函數的參數類型或返回類型不同,想到用tuple或者union。tuple取值要在編譯期間就確定,沒辦法在用戶交互過程中確定;而union要指定變數名,所以放到數組中按序號的選到之後,還是要指定變數才行,不符合要求。不過python里的tuple倒是可以直接從用戶的輸入中直接取值,試試用python吧。

#include&
#include&
#include&
using namespace std;
int 吃飯() {
return 1;
}
double 睡覺(int) {
return 2.0;
}
void 打豆豆(float) {
cout &(a)(1);//會報錯
get&<0&>(a)();
get&<1&>(a)(1);
get&<2&>(a)(3.3f);
}

python的tuple:

def a(x,y):
print(x+y)

def b():
print(6)

def c(x):
print(2*x)

d=(a,b,c) #d是tuple
d[0](1,2) #a
d[1]() #b
d[2](3) #c
n=2
d[n](3) #不會報錯,可以和用戶交互


當然有!我們偉大的Rust!

//先建立一張技能表,用來存放你所有的技能函數
struct SkillTable&
where T : FnMut()
{
table : Vec&&>,
}
impl& SkillTable&
where T : FnMut(){
fn new()-&>Self{
let mut table : Vec&&> = Vec::new();
SkillTable{
table,
}
}
fn push_function(mut self,skill_function:Box&){
self.table.push(skill_function);
}
fn get_function(self,index:usize) -&>Box&{
self.table[index]
}
fn skill_number(self) -&> usize{
self.table.len()
}
}
//然後建立一個英雄類,一個英雄對應一張技能表
struct Hero&
where T : FnMut(){
skills: SkillTable&,
hero : String,
}
impl& Hero&
where T : FnMut(){
fn new(skills:SkillTable&,hero:String)-&>Hero&{
Hero{
skills,
hero,
}
}
//技能發動:日語
fn hatudo(self,index:usize) -&> Box&{
self.skills.get_function(index)
}
}
//然後把我們寫好的技能函數放進來
//老財之呼吸——壹之型:背鍋
fn beiguo(){
println!("老財之呼吸——壹之型:都是財務的錯!")
}
//老財之呼吸——貳之型:跪舔
fn guitian(){
println!("老財之呼吸——貳之型:做財務要為公司想,我不下崗誰下崗")
}
//老財之呼吸——叄之型:默默付出
fn momofuchu(){
println!("老財之呼吸——叄之型:做財務就是要不爭不搶,就是不能向老闆提加工資")
}
//老財之呼吸——肆之型:吃香
fn chixiang(){
println!("老財之呼吸——肆之型:越老越吃香")
}
//然後就可以建立一個英雄了
fn hero_create(){
//給英雄起名!
let hero_name = String::from("老財");
//把技能函數加進去,建立技能表
let mut skt = SkillTable::new();
skt.push_function(Box::new(beiguo as fn()));
skt.push_function(Box::new(guitian as fn()));
skt.push_function(Box::new(momofuchu as fn()));
skt.push_function(Box::new(chixiang as fn()));
//把英雄和對應的技能表放到我們的英雄欄目裡面
let hero = Hero::new(skt,hero_name);
println!("Boss發動技能:
我看財務部很空嘛!
公司效益不好財務你們自己看著辦!
財務不產生效益!
要嚴肅批評財務!
你們肯定偷藏了利潤!");
//然後我們可以發動技能了
println!("老財發動技能:");
for i in (0..hero.skills.skill_number()){
hero.hatudo(i)();
}
}

效果如下:

PS:

當然我們也可以建立一份英雄清單,把建立好的多個英雄全部塞進去

//建立一個英雄清單
struct HeroList&
where T : FnMut()
{
hero_list : Vec&&>&>,
}
impl& HeroList&
where T : FnMut()
{
fn new() -&> Self{
let mut hero_list :Vec&&>&> = Vec::new();
HeroList{
hero_list,
}
}
fn push_new_hero(mut self,hero : Box&&>){
self.hero_list.push(hero);
}
}


推薦閱讀:
相关文章