<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Override on Tarragon</title><link>https://tarrragon.github.io/blog/tags/override/</link><description>Recent content in Override on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 30 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/override/index.xml" rel="self" type="application/rss+xml"/><item><title>late final 欄位不能用欄位覆蓋 — Dart 欄位的隱藏 getter/setter 機制</title><link>https://tarrragon.github.io/blog/work-log/late-final-%E6%AC%84%E4%BD%8D%E4%B8%8D%E8%83%BD%E7%94%A8%E6%AC%84%E4%BD%8D%E8%A6%86%E8%93%8B-dart-%E6%AC%84%E4%BD%8D%E7%9A%84%E9%9A%B1%E8%97%8F-getter/setter-%E6%A9%9F%E5%88%B6/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/late-final-%E6%AC%84%E4%BD%8D%E4%B8%8D%E8%83%BD%E7%94%A8%E6%AC%84%E4%BD%8D%E8%A6%86%E8%93%8B-dart-%E6%AC%84%E4%BD%8D%E7%9A%84%E9%9A%B1%E8%97%8F-getter/setter-%E6%A9%9F%E5%88%B6/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>&lt;code>AppService&lt;/code> 宣告了 &lt;code>late final PackageInfo packageInfo;&lt;/code>，在 &lt;code>init()&lt;/code> 中透過 &lt;code>PackageInfo.fromPlatform()&lt;/code> 非同步初始化。測試用的 &lt;code>TestAppService extends AppService&lt;/code> 想跳過平台呼叫，直接給一個固定值：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// ❌ analyzer 報錯：overridden_fields
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">late&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">PackageInfo&lt;/span> &lt;span class="n">packageInfo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PackageInfo&lt;/span>&lt;span class="p">(...);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dart analyzer 報 &lt;code>overridden_fields&lt;/code>：欄位覆蓋了從 &lt;code>AppService&lt;/code> 繼承的欄位。&lt;/p>
&lt;h2 id="根因late-final-欄位--getter--setter">根因：late final 欄位 = getter + setter&lt;/h2>
&lt;p>Dart 的每個 instance 欄位在底層都會生成對應的 getter（和非 final 的會生成 setter）。&lt;code>late final&lt;/code> 更特殊——它生成的 getter 包含「是否已初始化」的檢查邏輯，setter 包含「只能寫入一次」的 guard。&lt;/p>
&lt;p>當子類用另一個欄位覆蓋時，記憶體中會有兩個獨立的儲存槽：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>存取來源&lt;/th>
 &lt;th>讀到的 slot&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>子類自己的程式碼&lt;/td>
 &lt;td>子類的 slot（已初始化）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>父類的程式碼（繼承的方法）&lt;/td>
 &lt;td>父類的 slot（未初始化）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>父類的 &lt;code>init()&lt;/code> 或其他方法如果存取 &lt;code>packageInfo&lt;/code>，會讀到父類那份未初始化的 slot，拋出 &lt;code>LateInitializationError&lt;/code>。這就是為什麼 Dart 不允許用欄位覆蓋欄位。&lt;/p>
&lt;h2 id="修法改用-getter-覆寫">修法：改用 getter 覆寫&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">PackageInfo&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">packageInfo&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">PackageInfo&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nl">appName:&lt;/span> &lt;span class="s1">&amp;#39;UniPos Test&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nl">packageName:&lt;/span> &lt;span class="s1">&amp;#39;com.mxkj.unipos.test&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nl">version:&lt;/span> &lt;span class="s1">&amp;#39;1.0.0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nl">buildNumber:&lt;/span> &lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nl">buildSignature:&lt;/span> &lt;span class="s1">&amp;#39;test&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nl">installerStore:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Getter 覆寫只有一個讀取入口——不管父類還是子類的程式碼呼叫 &lt;code>packageInfo&lt;/code>，都走子類的 getter。不會有兩份 slot 的問題。&lt;/p>
&lt;h2 id="通則">通則&lt;/h2>
&lt;p>Dart 中覆寫父類的欄位，一律用 getter（必要時加 setter），不要用欄位。這不只適用於 &lt;code>late final&lt;/code>——所有欄位覆蓋都有同樣的雙 slot 風險，只是 &lt;code>late final&lt;/code> 的症狀最明顯（直接拋 &lt;code>LateInitializationError&lt;/code>），普通欄位的症狀更隱蔽（值不同步）。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p><code>AppService</code> 宣告了 <code>late final PackageInfo packageInfo;</code>，在 <code>init()</code> 中透過 <code>PackageInfo.fromPlatform()</code> 非同步初始化。測試用的 <code>TestAppService extends AppService</code> 想跳過平台呼叫，直接給一個固定值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// ❌ analyzer 報錯：overridden_fields
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">late</span> <span class="kd">final</span> <span class="n">PackageInfo</span> <span class="n">packageInfo</span> <span class="o">=</span> <span class="n">PackageInfo</span><span class="p">(...);</span></span></span></code></pre></div><p>Dart analyzer 報 <code>overridden_fields</code>：欄位覆蓋了從 <code>AppService</code> 繼承的欄位。</p>
<h2 id="根因late-final-欄位--getter--setter">根因：late final 欄位 = getter + setter</h2>
<p>Dart 的每個 instance 欄位在底層都會生成對應的 getter（和非 final 的會生成 setter）。<code>late final</code> 更特殊——它生成的 getter 包含「是否已初始化」的檢查邏輯，setter 包含「只能寫入一次」的 guard。</p>
<p>當子類用另一個欄位覆蓋時，記憶體中會有兩個獨立的儲存槽：</p>
<table>
  <thead>
      <tr>
          <th>存取來源</th>
          <th>讀到的 slot</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>子類自己的程式碼</td>
          <td>子類的 slot（已初始化）</td>
      </tr>
      <tr>
          <td>父類的程式碼（繼承的方法）</td>
          <td>父類的 slot（未初始化）</td>
      </tr>
  </tbody>
</table>
<p>父類的 <code>init()</code> 或其他方法如果存取 <code>packageInfo</code>，會讀到父類那份未初始化的 slot，拋出 <code>LateInitializationError</code>。這就是為什麼 Dart 不允許用欄位覆蓋欄位。</p>
<h2 id="修法改用-getter-覆寫">修法：改用 getter 覆寫</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">PackageInfo</span> <span class="kd">get</span> <span class="n">packageInfo</span> <span class="o">=&gt;</span> <span class="n">PackageInfo</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nl">appName:</span> <span class="s1">&#39;UniPos Test&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nl">packageName:</span> <span class="s1">&#39;com.mxkj.unipos.test&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nl">version:</span> <span class="s1">&#39;1.0.0&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nl">buildNumber:</span> <span class="s1">&#39;1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nl">buildSignature:</span> <span class="s1">&#39;test&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nl">installerStore:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">);</span></span></span></code></pre></div><p>Getter 覆寫只有一個讀取入口——不管父類還是子類的程式碼呼叫 <code>packageInfo</code>，都走子類的 getter。不會有兩份 slot 的問題。</p>
<h2 id="通則">通則</h2>
<p>Dart 中覆寫父類的欄位，一律用 getter（必要時加 setter），不要用欄位。這不只適用於 <code>late final</code>——所有欄位覆蓋都有同樣的雙 slot 風險，只是 <code>late final</code> 的症狀最明顯（直接拋 <code>LateInitializationError</code>），普通欄位的症狀更隱蔽（值不同步）。</p>
]]></content:encoded></item></channel></rss>