會寫這篇是因為上禮拜,有個資深的同事問我個問題,如何把底下的 programmer 素質拉上來? 跟他講這問題害我那天拖到晚上十點才回家吃晚飯 @_@,不過我想這也是現在台灣軟體業普遍碰到的人才問題,就順手寫了這篇。這篇是打算要貼在公司的 BLOG 裡給同事看的,這裡先貼一下,到時整理好再搬過去…。
今天同事有個問題搞不定,就找我去處理,越弄越火... 嘖嘖,這裡就來還原一下現場,記錄一下這個鳥問題...。
最討厭處理這種問題了。現在客戶 IT 都委外,結果就變成 IT 本身不大管事,什麼事就交給外包商就好... 而這種 A 包商跟 B 包商之間的問題,往往就變成踢皮球... 除非有明確證據指出人就是他殺的,不然? 出問題的一方自己摸摸鼻子吧。幾年來吃了不少這種虧,這就是小廠的悲哀啊。
這次碰到的例子是客戶 A 系統建的資料,需要整理後匯到我們負責維護的 B 系統。而中間資料需要作些修正,所以建了一個中繼資料庫,透過 LINKED SERVER,從 A 系統的 DB 把整個 TABLE SELECT 一份到中繼資料庫,然後再進行一連串的修正...
一開始問題很單純,就兩邊碰到中文字編碼不同,直接 SELECT 就碰到這樣的亂碼..
看起來是個小問題,請對方 IT 確認了編碼的問題後,我在中繼資料庫作了點調整,convert 成 ntext 就搞定了。已經可以跑出正確的中文資料了。
正想把程式弄一弄就收工,然後很得意的回報問題搞定時,發現不大對勁,怎麼整個 BATCH 跑下來結果還是錯的? 還錯的不一樣? 真是奇了... 原本轉 UNICODE 問題,再怎麼樣也應該只是變亂碼,或是變 ? 而以,結果這次看到的是資料錯亂,跑出其它的字出來...
這張圖是我把問題簡化後抓到的,原本是有上百行的 SQL SCRIPT ... @_@ 被我抽絲撥繭剩這段。第 33 筆資料是有問題的,不過出現的資料不是原本 "馥瑈" 啊,原本是第二個字變成 ? 而以... 現在竟然變成上一筆資料 (第32筆) 的內容,而第三個字 '榮' 則是由前面好幾筆的 "XX榮" 留下來,第四個字 "子" 就真的不曉得從那裡來了...
好怪的問題,如果是程式碰到這種 BUG,一看就像某個 BUFFER 沒清掉就一直重複被使用,後面的值直接蓋掉前面的值,字串長一點沒被蓋掉,就這樣留下來了。而到了第 33 這筆不知為什麼原因,整個 BUFFER 的內容就跑出來... 這段 SQL SCRIPT 跟本沒作什麼事,不過是前面的 SELECT 指令,改 SELECT INTO 到 TEMP TABLE,然後再撈出來而以。直接 SELECT 沒問題,沒道理 SELECT INTO 就掛掉啊! 不過還真的被我碰到了,see the ghost ..
我沒有直接拼下去 GOOGLE 或是試各種解法,而是想了一下問題是怎麼回事? SQL / DB 的東西我只算外行人,沒本事跟他硬碰硬,我也不知道有啥工具可以看 SQL NATIVE CLIENT 的 TRACE INFO 之類的,只能靠想像,猜一下問題會在那。一開始我就否定掉是不是什麼編碼或是定序的問題,因為我可以正確的 SELECT 出來啊,而且如果 SELECT INTO 是變成 ?? 的話我也許還會頭痛一點,不過竟然是上一筆的資料跑過來了? 這個問題很明顯的,跟本就是 overflow (緩衝區溢位) 之類的 BUG。八成是什麼地方應該填個 0x00 作字串結尾的 CODE 錯掉,結果 SQL 就抓過頭,抓到不該抓的舊資料才會這樣。
拿我寫了十幾年程式的經驗,跟它賭下去了!! 於是我就沒再去跟一堆編碼定序之類的設定搏鬥了,因為我認定這是 SQL SERVER 或是 SQL NATIVE CLIENT 的 BUG。我採的方案是找可以繞過去的方式,於是我把我想的到的 COPY TABLE 都用上了,最後試出來的是笨方法...
用... 用... 用 CURSOR 一筆一筆的跑... @_@
哈哈,各位看倌請笑小聲一點,我這貴州的驢子就只會這種把戲而以.... 果然這樣就正確了。CURSOR 的 FETCH 指令,把欄位的值抓到 nvarchar 的變數,就一切 OK,然後再把這變數的值 update 回我的暫存 TABLE,就什麼問題都沒了 -_-
真是它ㄨㄨㄨㄨ的,最後的 SOLUTION 細節我就不一一的貼出來了,反正只是換個方式 COPY TABLE 而以。既然我賭是 Microsoft 的問題,而換條路的方式 (完全沒改任何定序 OR 編碼) 也成功了,我就不跟它奮鬥下去了,客戶的 IT 既然懶的上 UPDATE / SERVICE PACK,我也只好避掉問題了事... 咳咳。特地貼出來記念一下,如果你們也碰到一樣的問題,切記切記,別跟它硬碰硬啊... :D
繼上篇,講了一些 yield return 編譯後產生的 Code, 說明了 C# compiler 如何用簡單的語法替你實作了 IEnumerator 介面,而完全不會增加程式的複雜度,這是我認為 C# 提供最讚的 Syntax Sugar ...。 不過無意間我想到了 yield return 還有另一種應用方式。靈感來自之前 Darkthread 舉辦的 [黑暗盃程式魔人賽]。因為參賽題目 [xAxB猜數字遊戲] 原本就是考驗演算法,邏輯就不大簡單了,加上要配合 GameHost 的呼叫方式,難度更提高不少。因此之前貼了兩篇文章 [ThreadSync #1. 概念篇 - 如何化被動為主動?, #2. 實作篇 - 互相等待的兩個執行緒],介紹了我改寫的 AsyncPlayer,讓程式可以分別以獨立的執行緒執行 GameHost 及 Player 的程式碼。藉著這方式讓兩者都可以 "獨立思考",邏輯不會中斷,讓程式能夠簡單一些。 不過執行緒同步機制是很花時間的,因為兩方都要等來等去...。多了 Sync 的動作,就要至少 10 ms 的時間來完成這動作。跑個十幾萬次下來,額外花費的時間太多了,因此我貼了那兩篇文章後,就一直在思考這樣的作法有沒有其它效能較佳的方式? 有的,最後我找到的答案就是 yield return,不過大家看了一定很納悶... "yield return (Iteration) 跟執行緒同步機制有什麼關聯?" 不多說,先看看之前畫的兩張時序圖: 先看之前 ThreadSync #1 裡提到的圖,我這次加上紅線當 "輔助線",紅線代表執行 GameHost 的主程式,這個執行序必需反反覆覆的在 GameHost / Player 兩份類別的程式碼跑來跑去,主程式是 GameHost 發起的,當然被強迫切成好幾段的就只有 Player 了。 這是修改過後的版本,GameHost / Player 有各自的執行緒,紅色是 GameHost,藍色是 Player。當執行緒跑到中間時代表它在等待了,等另一方也跑到中間把執行結果放到共用變數,同時叫醒對方之後才交換過來。兩方都各自照著自己的邏輯跑,不過這種等待 & 喚醒的動作,相較於一般的 function call / return 而言,實在是太慢了...。我就是從這張圖得來的靈感,這個解決方式不就跟 yield return 很像嘛? 都是為了避免多次呼叫之間,被呼叫的另一方的邏輯被破切斷的問題... 因此我就開始思考 AsyncPlayer 是不是有機會用 yield return 寫出另一個版本...。 原本的結構很直覺,透過共用變數來傳遞資訊,用 AutoResetEvent 來通知另一個等待中的執行緒可以醒來拿資料去用。而 yield return 則要換個角度來想這件事。yield return 是實作 Iterator 的一種方式,目的是讓你的程式自己決定如何把 collection 裡的 element 照什麼方式丟出去,原本的問題就要想成: "GameHost 要跟 Player 拿所有 Player 會問的問題,而 Player 會透過 yield return 一次一次的把問題丟出去。" 看起來好像可行,不過方向只有單向,就是 Player 丟問題給 GameHost,還缺了 GameHost 把問題答案交給 Player 這段。不過這部份好解決,一樣用共用變數就搞定。細節我就不講太多,直接來看程式碼: 用 yield return 改寫過的 AsyncPlayer
public abstract IEnumerable<HintRecord> Think(); private HintRecord last_record = null; public override int[] StartGuess(int maxNum, int digits) { base.StartGuess(maxNum, digits); this._enum = this.Think().GetEnumerator(); this._enum.MoveNext(); return this._enum.Current.Number; } public override int[] GuessNext(Hint lastHint) { this._enum.Current.Hint = lastHint; if (this._enum.MoveNext() == true) return this._enum.Current.Number; throw new InvalidOperationException("Player Stopped!"); } public override void Stop() { base.Stop(); this._enum.Current.Hint = new Hint(this._digits, 0); try { this._enum.MoveNext(); } catch { Console.WriteLine("!!!!"); } } protected virtual HintRecord GameHost_AskQuestion(int[] number) { this.last_record = new HintRecord( (int[])number.Clone(), new Hint()); return this.last_record; } protected HintRecord GameHostAnswer { get { return this.last_record; } }程式碼一如往常,又是只有一點點 (謎之音: 你到底有沒有寫過長一點的程式碼? -_-) ...。 原本的 Think 改成會傳回 IEnumerable<HintRecord> 的型別,因此內部就可以透過一連串的 yield return xxxx; 指令來把問題交給 GameHost。而 GameHost 拿到題目就會開始計算答案,然後再呼叫 Player.GuessNext( ) 把上次的答案傳回去。透過 Player 的實作,GuessNext 會呼叫 _enum.MoveNext( ), 控制權會再交到 Think( ) 上次呼叫 yield return 的地方,直到又執行到下一個 yield return 為止。這時 GameHost 又取得下一個問題,不斷重複這樣的動作直到結束。 同樣的,我們用 DummyPlayer 改寫,看看用 yield return 的版本寫起來是怎麼樣? DummyYieldPlayer 的程式碼
public class DummyYieldPlayer : YieldPlayer { private Random _rnd = new Random(); private int[] randomGuess() { int[] _currAnswer = new int[this._digits]; List<int> lst = new List<int>(); for (int i = 0; i < _digits; i++) { int r = _rnd.Next(_maxNum); while (lst.Contains(r)) r = _rnd.Next(_maxNum); lst.Add(r); _currAnswer[i] = r; } return _currAnswer; } public override IEnumerable<HintRecord> Think() { while (true) { yield return this.GameHost_AskQuestion(this.randomGuess()); } } }跟上次的 DummyAsyncPlayer (用 ThreadSync 的版本) 一樣,超簡單,實在沒什麼需要說明的了。唯一要特別記得的是,如果你需要取得 GameHost 傳回的答案,應該在 22 ~ 23 行之間,使用 this.GameHostAnswer( ) 來取得答案。有人問我為什麼不把它包成 function call ? 在 function 內接到參數後呼叫 yield return, 而把答案 return 回來不是很好嗎? 很無奈,除非 C# 支援像 C/C++ 那樣的 MACRO 語法,不然這個東西是不可能單靠 yield return 就做出來。你使用 yield return 的條件就是 function return type 一定要是 IEnumerable<T>,這是配對的,代表你不能任易的把 yield return 移到其它 function call 內。除非你不靠 C# yield return 來自動產生對應的 IEnumerator,一切自己來就可以。不過這樣不就又回到原點了? 咳咳... 就乖乖的寫兩行吧。 這樣的寫法執行效率就好的多,我用 DummyYieldPlayer 來測試,跟 DarkThread 提供的版本不相上下,意思是差異小到可以不理它的地步了 :D 這樣的方式不會有太大的效能損失,因為最後要執行的程式碼,跟直接手寫是差不多的,只是中間難寫的那段 code 是 C# compiler 幫我們解決掉,而不是像上回 AsyncPlayer 是用兩個執行緒來解決的。 效果很滿意,當然最後參賽的版本就改這寫法了 :D。不過寫的太晚,來不及幫到其它參賽者 :P,想到這方法算是我佔了 C# compiler 一點便宜,有幸找到方法坳 C# compiler 幫我把最難的部份寫好了,我自己則樂的輕鬆,專心研究怎樣才能少猜幾次... 這裡把我另類應用 yield return 的方法貼給各位參考一下,也算作個筆記 :D,各位高手如果還有發現 yield return 解決過你什麼樣的怪問題,也歡迎到我這留個言 :D
C# 常常拿來跟 Java 比較,在 .NET 1.1 時常常是不相上下,而 .NET 又因為較年輕 & 頂著 Microsoft 的名號,往往被當成玩具一樣,不過
Microsoft 的確是在 .NET 及 C# 下了很多功夫,作了很多 Sun 不願意在 Java 身上作的事,這次要探討
的 yield return 及 IEnumerable<T>
這搭配的 Interface 就是一例…。
Java 在過去的版本,往往為了跨平台,把修改 VM 規格視為大忌,連帶的連語法修改都一樣,即使不影響編譯出來的 bytecode 相容性也是 一樣不肯改。而 .NET 就為了語法簡潔,編譯器往往是讓步的一方,因此 C# 有相當多的 Syntax Sugar,讓你寫起 CODE 爽一點…。你只要寫簡單的 CODE,編譯器會幫你轉成 較煩雜的 CODE,有點像是文言文或是成語那樣的味道。古代文人常常用簡單的四個字,就有一大票引申的意義跑出來…,寫作文時只要 套上成語,就代表了成語背後的故事,寓意。
”yield return
” 算是最甜的甜頭了,因為編譯器替你翻出來的 code 整整一大串。先來看個簡單的例子,如果我想實作 IEnumerator<T>
Interface, 照順序輸出 1 ~ 100 的數字,正統的 C# code 看起來要像這樣:
用 IEnumerator 依序傳回 1 ~ 100 的數字
public class EnumSample1 : IEnumerator<int>
{
private int _start = 1;
private int _end = 100;
private int _current = 0;
public EnumSample1(int start, int end)
{
this._start = start;
this._end = end;
this.Reset();
}
public int Current
{
get { return this._current; }
}
public void Dispose()
{
}
object System.Collections.IEnumerator.Current
{
get { return this._current; }
}
public bool MoveNext()
{
this._current++;
return !(this._current > this._end);
}
public void Reset()
{
this._current = 0;
}
}
好不容易寫好 IEnumerator
之後,再來是拿來用,一筆一筆印出來:
取得 IEnumerator
物件後,依序取出裡面的數字
EnumSample1 e = new EnumSample1(1, 100);
while (e.MoveNext())
{
Console.WriteLine("Current Number: {0}", e.Current);
}
不過如果只是要列出 1 ~ 100,大部份的人都不會想這樣寫吧? 直接用計概第一堂教你的 loop 不就好了? 程式碼如下:
送分題: 用 LOOP 印出 1 ~ 100 的數字
for (int current = 1; current <= 100; current++)
{
Console.WriteLine("Current Number: {0}", current);
}
兩個範例都沒錯啊,那為什麼要用 IEnumerator
? 其實 IEnumerator
並不是 Microsoft 發明的,在四人幫寫的
經典書籍 (Design Patterns) 裡就有這麼一個設計模式: Iterator,它的目的
很明確:
“毋須知曉聚合物件的內部細節,即可依序存取內含的每一個元素。”
(摘自 物件導向設計模式 Design Patterns 中文版,葉秉哲 譯)
這裡指的 “聚合物件” 就是指 .NET 的 Collection
, List
, Array
等這類物件。意思是你不需要管 collection 裡每一個物件是怎麼擺的,用什麼結構處理的,用什麼邏輯或演算法處理的,我就只管照你安排好的順序一個一個拿出來就好。沒錯,這就是它主要的目的。換
另一個說法,就是我們希望把物件巡訪的順序 (iteration) 跟依序拿到物件後要作什麼事 (process) 分開,那你就得參考 Iterator Pattern。
不用? 那只好讓你的 iteration / process 混在一起吧。
差別在那? 我們再來看第二個例子。如果題目改一下,要列出 1 ~ 100 的數字,但如果不是 2 的倍數,也不是 3 的倍數,就跳過去。先來 看看 Loop 的版本:
進階送分題,用LOOP印出 1~100 之中,2 或 3 的倍數
for (int current = 1; current <= 100; current++)
{
bool match = false;
if (current % 2 == 0) match = true;
if (current % 3 == 0) match = true;
if (match == true)
{
Console.WriteLine("Current Number: {0}", current);
}
}
再來看看 IEnumerator
的版本:
用 IEnumerator
列出 1 ~ 100 中 2 或 3 的倍數
public class EnumSample2 : IEnumerator<int>
{
private int _start = 1;
private int _end = 100;
private int _current = 0;
public EnumSample2(int start, int end)
{
this._start = start;
this._end = end;
this.Reset();
}
public int Current
{
get { return this._current; }
}
public void Dispose()
{
}
object System.Collections.IEnumerator.Current
{
get { return this._current; }
}
public bool MoveNext()
{
do {
this._current++;
} while(this._current %2 > 0 && this._current %3 > 0);
return !(this._current > this._end);
}
public void Reset()
{
this._current = 0;
}
}
而扣掉 IEnumerator
的部份,要把數字印出來的程式碼則完全沒有改變:
取出 IEnumerator
的每個數字,印到畫面上
EnumSample2 e = new EnumSample2(1, 100);
while (e.MoveNext())
{
Console.WriteLine("Current Number: {0}", e.Current);
}
可以看的到,Loop 版本的確是把 iteration 跟 process 的 code 完全混在一起了,未來任何一方的邏輯要抽換都很
麻煩,而 IEnumerator
則不會,分的很清楚,不過… 這 Code 會不會太 “髒” 了一點啊…? 試問一下,有誰會這麼
勤勞,都用 IEnumerator
來寫 Code? 有的話請留個言,讓我崇拜一下…。
屁話講了一堆,最後就是要帶出來 “的確有魚與熊掌得兼的方法”,怎麼作? 來看看用 C# 的 yield return
版本的程式碼:
傳回 IEnumerable
的 METHOD (不用再寫 CLASS,實作 IEnumerator 了)
public static IEnumerable<int> YieldReturnSample3(int start, int end)
{
for (int current = 1; current <= 100; current++)
{
bool match = false;
if (current % 2 == 0) match = true;
if (current % 3 == 0) match = true;
if (match == true)
{
yield return current;
}
}
}
用 foreach 搭配 IEnumerable
印出每一筆數字
foreach (int current in YieldReturnSample3(1, 100))
{
Console.WriteLine("Current Number: {0}", current);
}
真是太神奇了,安德魯。如何? 完美的結合兩者的優點,這種 code 實在是令人挑不出什麼缺點… 真是優雅…
不過念過 系統程式 的人一定都會吶悶… 這樣的程式執行方式,不就
完全的違背了一般結構化程式典型的 function call / return 的鐵律了? 程式呼叫某個 function 就應該完全執行完
才能 return 啊,怎麼能 yield return
後,跑完一圈又回到剛才執行到一半的 function 繼續跑,然後再 yield return
?
好像同實有兩段獨立的邏輯在運作… 還可以在兩者之間跳來跳去?
這就是 C# compiler 猛的地方了。搬出 reflector 來看看編譯出來的 code, 再被反組譯回來變成什麼樣子:
反組譯 YieldReturnSample3
public static IEnumerable<int> YieldReturnSample3(int start, int end)
{
<YieldReturnSample3>d__0 d__ = new <YieldReturnSample3>d__0(-2);
d__.<>3__start = start;
d__.<>3__end = end;
return d__;
}
耶? 看到一個多出來的 class: <YieldReturnSample3>d__0
… 再看看它的 class 長啥樣:
編譯器自動產生的 IEnumerator 衍生類別
[CompilerGenerated]
private sealed class <YieldReturnSample3>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private int <>2__current;
public int <>3__end;
public int <>3__start;
private int <>l__initialThreadId;
public int <current>5__1;
public bool <match>5__2;
public int end;
public int start;
// Methods
[DebuggerHidden]
public <YieldReturnSample3>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<current>5__1 = 1;
while (this.<current>5__1 <= 100)
{
this.<match>5__2 = false;
if ((this.<current>5__1 % 2) == 0)
{
this.<match>5__2 = true;
}
if ((this.<current>5__1 % 3) == 0)
{
this.<match>5__2 = true;
}
if (!this.<match>5__2)
{
goto Label_0098;
}
this.<>2__current = this.<current>5__1;
this.<>1__state = 1;
return true;
Label_0090:
this.<>1__state = -1;
Label_0098:
this.<current>5__1++;
}
break;
case 1:
goto Label_0090;
}
return false;
}
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
Program.<YieldReturnSample3>d__0 d__;
if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
{
this.<>1__state = 0;
d__ = this;
}
else
{
d__ = new Program.<YieldReturnSample3>d__0(0);
}
d__.start = this.<>3__start;
d__.end = this.<>3__end;
return d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
// Properties
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
耶? 不就完全跟之前手工寫的 IEnumerator
一樣嘛? 只不過這個 IEnumerator
是自動產生出來的,不是手寫的…。 畢竟是機器
產生的 CODE,總是沒那麼精簡。想到了嗎? 沒錯,這就是 C# compiler 送給你的 syntax sugar …,你可以腦袋裡想像著計概課
入門時教你的 LOOP 那樣簡單的想法,compiler 就幫你換成 IEnumerator
的實作方式,讓你隨隨便便就可以跟別人宣稱:
看! 我的程式有用到 Iterator 這個設計模式喔…
聽起來好像很臭屁的樣子… 哈哈! 如果是在真的用的到 Iterator Patterns 的
情況下,真的是可以很臭屁的拿出來炫耀一下。不過,我幹嘛突然講起 yield return
? 各位看的過程中有沒有聯想到
前幾篇 POST 講的 Thread Sync 那兩篇文章 ( #1,
#2 ) ? IEnumerator
跟 Thread Sync 又有什麼關係? 賣個關子,下篇繼續!
話說前陣子處理了 BlogEngine.NET 升級到 1.4.5.0,另外也寫了 SecurePost.cs 這個 extension, 其時都碰過這個鳥問題,只是一直沒去理它而以。接下來為了要改 PostViewCounter.cs (BE extension, too), 又碰到... 於是就認真的研究了一下...。
過程是這樣,為了建立 BlogEngine 的開發環境,首先我從官方網站下載了 source code, 解開後編譯都沒問題,OK。
接下來 WEB 的部份我把網站上的 source code 搬過來 (不包含 ~/App_Data,太大了),編譯也 OK。
不過我要改 Counter 的 Code 啊,沒有一些 SAMPLE DATA 很難測試,只好把資料檔也搬過來.. 結果 Visual Studio 2008 就冷冷的回了這訊息給我:
(0): Build (web): The CodeDom provider type "Microsoft.VJSharp.VJSharpCodeProvider, VJSharpCodeProvider, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" could not be located.
我沒有漏貼前面的訊息... 的確是沒有檔名,也沒有行號(0)。我最不能忍受的就是沒頭沒尾的 ERROR MESSAGE 了。除了告訴你 "掛掉了" 之外,無頭無腦的對於追查問題實在沒什麼幫助。只好靠自己了...。雖然這是個 compile error message,不過我要 RUN 的畢竟是個 web site, 不編譯也是可以跑,除了那個惱人的錯誤訊息之外,要執行倒是沒問題。只不過編譯失敗,我就不能設中斷點,直接 F5 執行測試。雖然可以另外手動 Attach Process 的方式來除錯,不過每次都要這樣搞實在是很煩..
仔細想了想,沒錯,我是沒裝 Visual J#。不過我的確沒要用 Visual J# 啊,如果真的用到 J# 的話,出這訊息是應該的。訊息沒有原始檔? 也沒有錯誤行號? 那問題應該是 Global 的範圍,第一個想到的就是 web.config 是不是定義了 CodeDom 或是指定了相關的 CodeProvider ? 無奈查了一遍沒看到,VS2008 的 PROJECT 設定也沒看到引用任何 J# 相關的 LIB...
已經到了死馬當活馬醫的地步... 開始亂找一通碰碰運氣。搜尋了一下有沒有 *.java 的檔? OUCH,還真的有... 在 ~/App_Data/files 下找到我古董檔案,研究所時代寫的 Java Applet .... 順手試一下,刪掉後還真的就過了? 這個無頭無腦的問題,就在不知不覺中找到 solution, case closed!
怒... 這樣也算? 找到 .java 的程式碼,去找 VJ# 來編譯還說的過去,不過找 "source code" 找到 ~/App_Data 實在是太超過了一點... 好歹也列個要編譯那個檔案,然後找不到對應的 CodeProvider,這樣要排除問題也簡單一點...
結論是: 各位別太鐵齒,看來 ~/App_Data 下的檔案也是不能亂塞的...