主頁 > 後端開發 > 答應我,別在go專案中用init()了

答應我,別在go專案中用init()了

2021-04-11 06:14:28 後端開發

前言

goinit函式給人的感覺怪怪的,我想不明白聰明的 google團隊為何要設計出這么一個“雞肋“的機制,實際編碼中,我主張盡量不要使用init函式,

首先來看看 init函式的作用吧,

init() 介紹

init()與包的初始化順序息息相關,所以先介紹一個go中包的初始化順序吧,(下面的內容部分摘自《The go programinng language》)

大體而言,順序如下:

  1. 首先初始化包內宣告的變數
  2. 之后呼叫 init 函式
  3. 最后呼叫 main 函式
變數的初始化順序
變數的初始化順序由他們的依賴關系決定

應該任何強型別語言都是這樣子吧,

例如:

var a = b + c;
var b = f();	// 需要呼叫 f() 函式
var c = 1
func f() int{return c + 1;}

a 依賴 bcb 依賴 f()f() 依賴 c,因此,他們的初始化順序理所當然是 c -> b -> a

graph TB; b-->a c-->a f-->b c-->b

Ps:其實在這里可能引申出一個沒用的小技巧,當你有一個函式需要在包被初始化的程序中被呼叫時,你可以把這個函式賦值給一個包級變數,這樣,當包被初始化時就會自動呼叫這個函式了,這個函式甚至能夠在 init() 之前被呼叫!不過話說回來,它既然比 init() 更早被呼叫,那它才是真正的 init() 才對;此外你也可以在 init() 中呼叫該函式,這樣才更合理一些,

// 笨版
// 函式必須得有一個回傳值才行
var _ = func() interface{} {
	fmt.Println("hello")
	return nil
}()

func init() {
	fmt.Println("world")
}

func main() {

}
// Output:
// hello
// world
// 更合理的版本
func init() {
	fmt.Println("hello")
	fmt.Println("world")
}

func main() {

}
// Output:
// hello
// world
包內變數的初始化順序

一個包內往往有多個 go檔案,這么go檔案的初始化順序由它們被提交給編譯器的順序決定,順序和這些檔案的名字有關,

init()

主角出場了,先來看看它的設計動機吧:

Each variable declared at package level starts life with the value of its initializer expression, if any, but for some variables, like tables of data,an initializer expression may not be the simplest way to set its initial value.In that case,the init function mechanism may be simpler. 《The go pragramming language P44》

這句話的意思是有的包級變數沒辦法用一條簡單的運算式來初始化,這個 時候,init機制就派上用場了,

init() 不能被呼叫,也不能被 reference,它們會在程式啟動時自動執行,

同一個 go 檔案中 init 函式的呼叫順序

一個包內,甚至 go 檔案內可以包含多個 init(),同一個 go 檔案中的 init() 呼叫順序由他們的宣告順序決定 ,

func init() {
	fmt.Print("a")
}
func init() {
	fmt.Print("b")
}
func init() {
	fmt.Print("c")
}
// Output
// abc
同一個包下面不同 go 檔案中 init() 的呼叫順序

依舊是由它們的宣告順序決定,同一個包下面的所有go 檔案在編譯時會被編譯器合并成一個“大的go檔案“(并不是真正合并,僅僅是效果類似而已),合并的順序由編譯器決定,

不要把程式是否能夠正常作業寄托在init()能夠按照你期待的順序被呼叫上,

不過話說回來,正經人誰在一個包里寫很多 init() 呀,而且還把這些 init() 放在不同檔案里,更可惡的是每個檔案里還有多個 init(),要是看到這樣的代碼,我立馬:@#$%^&*...balabala...

一個包里最多寫一個init()(我甚至覺得最好連一個 init() 都不要有)

不同包內 init 函式的呼叫順序

唯獨這個順序,我們程式員是絕對可控的,它們的呼叫順序由包之間的依賴關系決定,假設 a包需要 import b包,b包需要import c包,那么很顯然他們的呼叫順序是,c包的init()最先被呼叫,其次是b包,最后是a包,

graph LR c-->b b-->a
一個包的init函式最多會被呼叫一次

道理類似于一個變數最多會被初始化一次,

有的同學會問,一個變數明明可以多次賦值呀,可第二次對這個變數賦值那還能夠叫初始化么?

例如有如下的包結構,B包和C包都分別import A包,D包需要import B包和C包,

graph TD; A-->B A-->C B-->D C-->D

A包中有 init()

func init() {
	fmt.Println("hello world")
}

D包是 main 包,最終程式只輸出了一句 hello world

我不喜歡 init 函式的原因

我不喜歡 init 函式的一個重要原因是,它會隱藏掉程式的一些細節,它會在沒有經過你同意的情況下,偷偷干一些事情,go 的函式王國里,所有的函式都需要程式員顯示的呼叫(Call)才會被執行,只有它——init(),是個例如,你明明沒 Call 它,它卻偷偷執行了,

有的同學會說,c++ 里類的建構式也是在物件被創建時就會默默執行呀,確實是這樣,但在 c++ 里,當你點進這個類的定義時,你就能立馬看到它的建構式和解構式,在 go 里,當你點進某個包時,你能立馬看到包內的init()么?這個包有沒有init()以及有幾個init()完全是個未知數,你需要在包內的所有檔案中搜索 init() 這個關鍵字才能摸清包的 init()情況,而大多數人包括我懶得費這個功夫,在c++中創建物件時,程式員能夠很清楚的意識到這個操作會觸發這個類的建構式,這個建構式的內容也能很快找到;但在 go 中,import 包時,一切卻沒那么清晰了,

希望將來 goland 或者 vscode 能夠分析包內的 init() 情況,這樣我對 init() 的惡意會減半,

init() 給專案維護帶來的困難

當你看到這樣的 import 代碼時

import(
	_ "pkg"
)

你立馬能夠知道,這個 import 的目的就是呼叫 pkg 包的 int()

當看到

import(
	"pkg"
)

你卻很難知道,pkg 包里藏著一個 init(),它被偷偷呼叫了,

但這還好,你起碼知道如果 pkg 包有 init() 的話,它會在此處被呼叫,

但當pkg 包,被多個包 import 時,pkg 包內的 init() 何時被呼叫的,就是一個謎了,你得搞清楚這些包之間的 import 先后順序關系,這是一場噩夢,

使用 init()的時機

先說一下我的結論:我認為 init()應該僅被用來初始化包內變數,

《The go programming language》提供了一個使用 init函式的例子,

// pc[i] 是 i 中 bit = 1 的數量
var pc [256]byte

func init() {
	for i := range pc {
		pc[i] = pc[i/2] + byte(i&1)
	}
}

// 回傳 x 中等于 1 的 bit 的數量
func PopCount(x uint64) int {
	return int(pc[byte(x>>(0*8))] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}

PopCount 函式的作用數計算數字中等于 1bit 的數量,例如 :

var i uint64 = 2

變數 i 的二進制表示形式為

0000000000000000000000000000000000000000000000000000000000000010

把它傳入 PopCount 最終得到的結果將為 1,因為它只有一個 bit 的值為 1

pc 是一個表,它的 index 為 x,其中 0 <= x <= 255,value 為 x 中等于 1 的 bit 的數量,

它的初始化思想是:

  1. 如果一個數x最后的 bit 為 1,那么這個數值為 1 的bit數 = x/2 的值為1的bit數 + 1;

  2. 如果一個數x最后的 bit 為 0,那么這個數值為 1 的bit數 = x/2 的值為1的bit數;

PopCount 中把一個 8byte 數拆成了 8 個單 byte 數,分別計算這8個單 byte 數中 bit1 的數量,最后累加即可,

這里 pc 的初始化確實比較復雜,無法直接用

var pc = []byte{0, 1, 1,...}

這種形式給出,

一個可以替代 init()的方法是:

var pc = generatePc()

func generatePc() [256]byte {
	var localPc [256]byte
	for i := range localPc {
		localPc[i] = localPc[i/2] + byte(i&1)
	}
	return localPc
}

我覺得這樣子初始化比利用 init() 初始化要更好,因為你可以立馬知道 pc 是怎樣得來的,而利用 init() 時,你需要利用 ide 來查找 pc 的 write reference,之后才能知道,哦,原來它(pc)來這里(init()) 被初始化了呀,

當包內有多個變數的初始化流程比較復雜時,可能會寫出如下代碼,

var pc = generatePc()
var pc2 = generatePc2()
var pc3 = generatePc3()
// ...

有的同學可能不太喜歡這種寫法,那么用上 init() 后,會寫成這樣

func init() {
	initPc()
	initPc2()
	initPc3()
}

我覺得兩種寫法都說的過去吧,雖然我個人更傾向第一種寫法,

使用 init()的時機,僅僅有一個例外,后面說,

不使用 init 函式的時機

init()除了初始化變數,不應該干其他任何事!

有兩個原則:

  1. 一個包的 init() 不應該依賴包外的環境
  2. 一個包的 init() 不應該對包外的環境造成影響

設定這兩個原則的理由是:任何對外部有依賴或者對外部有影響的代碼都有義務顯式的讓程式員知曉,不應該自己悄咪咪地去做,最好是顯式地讓程式員自己去呼叫,

init() 的活動范圍就應該僅僅被局限在包內,自己和自己玩,不要影響了其他小朋友的游戲體驗,

如下幾條行為就踩了紅線:

  1. 讀取配置(依賴于外部的組態檔,且一般讀取配置得到的 obj 會被其他包訪問,違反了第一條和第二條)
  2. 注冊路由(因為修改了 http 包中的 routeMap,會對 http 包造成影響,違反了第二條)
  3. 連接資料庫(連接資料庫后一般會得到一個 db 物件給業務層去curd吧?違反了第二條)
  4. etc... 我暫時只能想到這么多了

一個反面教材 https://github.com/go-sql-driver/mysql

反面教材就是:https://github.com/go-sql-driver/mysql 這個大名鼎鼎的包

當使用這個包時,一個必不可少的陳述句是:

import (
	_ "github.com/go-sql-driver/mysql"
)

原因是它里面有個 init函式,會把自己注冊到 sql 包里,

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

按照之前的標準,此處明顯不符合規范,因為它影響了標準庫的 sql 包,

我認為一個更好的方法是,創建一個 export 的專門用來做初始化作業的方法:

// Package mysql
func Init() {
	sql.Register("mysql", &MySQLDriver{})
}

然后在 main 包中顯式的呼叫它:

// Package main
func main(){
    mysql.Init();
    // other logic
}

來比較一下兩種方式吧,

  1. 使用 Init()

    • 是否需要告訴開發者額外的資訊?

      需要,

      需要告訴開發者:使用這個庫時,記得一定要呼叫 Init() 哦,我在里面做了一些作業,

      開發者,點進 Init(),瞬間了然,

    • 是否能夠阻止開發者不正確的呼叫?

      不能,

      因為是 export 的,所以開發者可以想到哪兒呼叫就到哪兒呼叫,想呼叫多少次就呼叫多少次,

      因此需要額外告訴開發者:請您務必只呼叫一次,之后就不要呼叫了,且必須在用到 sql 包之前呼叫,一般而言都是在 main() 的第一句呼叫,

  2. 使用 init()

    • 是否需要告訴開發者額外的資訊?

      需要

      依舊需要告訴開發者,一定要用 _ "github.com/go-sql-driver/mysql"這個陳述句顯式的匯入包哦,因為我利用init()在里面做一些作業,

      開發者:那你做了什么作業

      庫:親,請您點進 mysql 包,在目錄下搜索 init() 關鍵字,慢慢找哦,

      開發者:......

    • 是否能夠阻止開發者不正確的呼叫?

      勉強可以吧,

      因為 init() 只會被呼叫一次,不可能被呼叫多次,這從根本上杜絕了開發者呼叫多次的可能性,

      可你管不了開發者的 import 時機,假設開發者在其他地方 import 了,導致你在 sql.Open()時,mysqldriver 沒有被正常注冊,你還是拿開發者沒有辦法,只能哀嘆一聲:我累了,毀滅吧,

我覺得作為庫的提供者,最主要的是提供完善的機制,在用戶使用你的庫時,能利用你提供的機制,寫出無bug 的代碼,而不是像保姆一樣,想方設法避免用戶出錯,

所以可能使用 init() 為了的優勢就是減少了代碼量吧,

使用 Init() 時,需要兩句代碼

import (
	"github.com/go-sql-driver/mysql"	// 這句
)

func main(){
    mysql.Init()				  // 這句
}

但是使用 init 時,卻只需要一句代碼

import (
	_ "github.com/go-sql-driver/mysql"	// 這句
)

oh yeah,足足少寫了一句代碼!

一個例外 單元測驗

可能使用 init 的唯一例外就是寫單元測驗的時候了吧,

假設我現在需要需要對 dao 層的增刪改查邏輯的寫一個單元測驗,

func TestCURDPlayer(t *testing.T) {
	// 測驗 curd 玩家資訊
}

func TestCURDStore(t *testing.T) {
	// 測驗 curd 商店資訊
}

func TestCURDMail(t *testing.T) {
	// 測驗 curd 郵件資訊
}

很顯然,這些測驗都是依賴資料庫的,因此為了正常的測驗,必須初始化資料庫

func TestCURDPlayer(t *testing.T) {
	// 測驗 curd 玩家資訊
    initdb()
    // balabala
}

func TestCURDStore(t *testing.T) {
	// 測驗 curd 商店資訊
    initdb()
    // balabala
}

func TestCURDMail(t *testing.T) {
	// 測驗 curd 郵件資訊
    initdb()
    // balabala
}

func initdb(){
    // sql.Open()...
}

難道我每次新增一個單元測驗,都要在單元測驗的代碼中加一個 initdb() 么,這也太麻煩了吧,

這個時候 init() 就派上用場了,可以這樣

func TestCURDPlayer(t *testing.T) {
	// 測驗 curd 玩家資訊
    // balabala
}

func TestCURDStore(t *testing.T) {
	// 測驗 curd 商店資訊
    // balabala
}

func TestCURDMail(t *testing.T) {
	// 測驗 curd 郵件資訊
    // balabala
}

func init(){
    initdb()
}

func initdb(){
    // sql.Open()...
}

這樣,當對這個檔案進行單元測驗時,可以確保在執行每個 TestXXX 函式時,db 肯定是被正確初始化了的,

那為什么這個地方可以利用 init() 來初始化資料庫呢?

理由之一是它的影響范圍很小,僅僅在 xxx_test.go 檔案中生效,在 go run 時不會起作用,在 go test 時才會起作用,

理由之二是我懶,,,

總結

init 更像是一個語法糖,它會讓開發者對代碼的追蹤能力變弱,所以能不用就最好不用,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274680.html

標籤:其他

上一篇:Java物件的創建程序

下一篇:畢業設計SpringBoot+Vue仿百度網盤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more