[{"content":"做法 22 支援泛型的共變數與反變數\n1. 這個 out 也是 共變數? public static bool TryParse (string? s, IFormatProvider? provider, out int result); 方法參數中的 out 關鍵字： 在 TryParse 方法中的 out int result 是一個輸出參數。這個 out 關鍵字的作用是：\n表示這個參數是一個輸出參數。 方法必須在返回前為這個參數賦值。 呼叫方法時，不需要初始化這個參數，但必須在呼叫後使用它。 在泛型接口或委託定義中使用的 out 關鍵字表示協變。例如：\ninterface IEnumerable\u0026lt;out T\u0026gt; { ... } 這裡的 out 表示：\nT 只能用作輸出（返回值），不能用作輸入（參數）。 允許將更具體（派生）類型的集合賦值給更一般（基類）類型的集合變量。 區別總結：\n用途不同： 方法參數的 out：用於指定輸出參數。 泛型的 out：用於指定類型參數的協變性。 位置不同： 方法參數的 out：用在方法參數列表中。 泛型的 out：用在泛型類型參數定義中。 功能不同： 方法參數的 out：確保方法將值賦給參數。 泛型的 out：允許更靈活的類型轉換和賦值。 編譯時行為： - 方法參數的 out：編譯器確保方法內部對參數賦值。 - 泛型的 out：編譯器允許特定的類型轉換。 所以， TryParse 方法中的 out 不是用於協變的，而是一個輸出參數標記。這兩種 out 雖然使用相同的關鍵字，但在 C# 中代表完全不同的概念。 簡單解釋共變數與反變數 共變數（Covariance） 定義：允許使用比原本指定的類別更具體（更繼承）的類型。 關鍵字：out 應用：通常用於返回類別或只讀泛型介面。\nIEnumerable\u0026lt;string\u0026gt; strings = new List\u0026lt;string\u0026gt;(); IEnumerable\u0026lt;object\u0026gt; objects = strings; // 合法，因為 IEnumerable\u0026lt;T\u0026gt; 是共變數 反變數（Contravariance） 定義：允許使用比原本指定的類別更一般（更基礎）的類別。 關鍵字：in 應用：通常用於參數類別或只寫泛型介面。\nAction\u0026lt;object\u0026gt; objectAction = (object obj) =\u0026gt; Console.WriteLine(obj); Action\u0026lt;string\u0026gt; stringAction = objectAction; // 合法，因為 Action\u0026lt;T\u0026gt; 是反變數 詳細解釋和更多例子 共變數例子：\npublic class Animal { } public class Dog : Animal { } IEnumerable\u0026lt;Dog\u0026gt; dogs = new List\u0026lt;Dog\u0026gt;(); IEnumerable\u0026lt;Animal\u0026gt; animals = dogs; // 共變數允許這種賦值 // 使用場景 void ProcessAnimals(IEnumerable\u0026lt;Animal\u0026gt; animals) { ... } ProcessAnimals(dogs); // 可以傳入 Dog 的集合 反變數例子：\npublic interface IComparer\u0026lt;in T\u0026gt; { int Compare(T x, T y); } IComparer\u0026lt;Animal\u0026gt; animalComparer = new AnimalComparer(); IComparer\u0026lt;Dog\u0026gt; dogComparer = animalComparer; // 反變數允許這種賦值 // 使用場景 class AnimalComparer : IComparer\u0026lt;Animal\u0026gt; { public int Compare(Animal x, Animal y) { ... } } List\u0026lt;Dog\u0026gt; dogs = new List\u0026lt;Dog\u0026gt;(); dogs.Sort(dogComparer); // 可以使用 Animal 的比較器來比較 Dog 共變數和反變數的組合\npublic interface IConverter\u0026lt;in TInput, out TOutput\u0026gt; { TOutput Convert(TInput input); } IConverter\u0026lt;Animal, Dog\u0026gt; animalToDog = new AnimalToDogConverter(); IConverter\u0026lt;Dog, Animal\u0026gt; dogToAnimal = animalToDog; // 合法 限制和注意事項 共變數和反變數只適用於引用類型，不適用於值類型。 只能用於介面和委託的定義，不能用於類別。 一個類別參數不能同時標記為共變數和反變數。 共變數類別參數不能用作方法參數，反變數類別參數不能用作方法返回類型。 實際應用 LINQ: LINQ 大量使用共變數和反變數來提高其靈活性。 事件處理：允許更靈活地處理不同類別的事件。 依賴注入：在依賴注入框架中常用，提高容器的靈活性。 性能考慮 共變數和反變數主要是編譯時特性，不會對運行時性能產生顯著影響。 總結： 共變數和反變數強大的特性，能夠提高程式碼的靈活性和可重用性。 共變數（out）允許使用更具體的類型，而反變數（in）允許使用更一般的類型。正確理解和使用這些概念可以幫助設計更靈活、更強大的 API 和框架。 static void Main(string[] args) { Dog dog = new Dog(\u0026#34;doggy\u0026#34;); IOutDog\u0026lt;Dog\u0026gt; dogProvider = new DogProvider(dog); IOutDog\u0026lt;Animal\u0026gt; animalProvider = dogProvider; Console.WriteLine($\u0026#34;{animalProvider.Value.GetName()}, {dogProvider.Value.GetName()}\u0026#34;); // 使用基本的 AnimalSetter IInDog\u0026lt;Animal\u0026gt; animalSetter = new AnimalSetter(); animalSetter.SetValue(new Animal()); animalSetter.SetValue(new Dog()); // 這也是有效的，因為 Dog 是 Animal 的子類 // 使用 DogSetter，展示反變數 // IInDog\u0026lt;Animal\u0026gt; dogSetterAsAnimal = (IInDog\u0026lt;Animal\u0026gt;)new DogSetter(); // dogSetterAsAnimal.SetValue(new Animal()); // 注意：這在運行時可能會拋出異常，因為實際的實現期望一個 Dog // 使用泛型實現 IInDog\u0026lt;Dog\u0026gt; dogSetter = new GenericAnimalSetter\u0026lt;Dog\u0026gt;(); dogSetter.SetValue(new Dog()); IConverter\u0026lt;Animal, Dog\u0026gt; animalToDog = new AnimalToDogConverter(); IConverter\u0026lt;Dog, Animal\u0026gt; dogToAnimal = animalToDog; // 合法 } public class Animal { private readonly string name; public Animal(string name = \u0026#34;\u0026#34;) =\u0026gt; this.name = name; public virtual string GetName() =\u0026gt; name; } public class Dog : Animal { public Dog(string name = \u0026#34;\u0026#34;) : base(name) { } public override string GetName() =\u0026gt; $\u0026#34;Dog {base.GetName()}\u0026#34;; } public interface IOutDog\u0026lt;out T\u0026gt; where T : Animal { T Value { get; } } public class DogProvider : IOutDog\u0026lt;Dog\u0026gt; { public Dog Value { get; set; } public DogProvider(Dog value) =\u0026gt; Value = value; } public interface IInDog\u0026lt;in T\u0026gt; where T : Animal { void SetValue(T value); } public class AnimalSetter : IInDog\u0026lt;Animal\u0026gt; { private Animal value; public void SetValue(Animal value) =\u0026gt; this.value = value; } public class DogSetter : IInDog\u0026lt;Dog\u0026gt; { private Dog value; public void SetValue(Dog value) =\u0026gt; this.value = value; } public class GenericAnimalSetter\u0026lt;T\u0026gt; : IInDog\u0026lt;T\u0026gt; where T : Animal { private T value; public void SetValue(T value) =\u0026gt; this.value = value; } public interface IConverter\u0026lt;in TInput, out TOutput\u0026gt; { TOutput Convert(TInput input); } public class AnimalToDogConverter : IConverter\u0026lt;Animal, Dog\u0026gt; { public Dog Convert(Animal input) =\u0026gt; new Dog(input.GetName()); } 心得 如果編譯器告訴你錯了 就要小心是不是有什麼地方搞錯了 做 cast 時要了解自己在做什麼 不然能編譯成功也會在 runtime 時炸掉\n做法 23 使用 delegate 定義型別參數的方法約束\n// double[] xValues = { 0,1,2,3,4,5,6,7,8,9, // 0,1,2,3,4,5,6,7,8,9,}; // double[] yValues = { 0,1,2,3,4,5,6,7,8,9, // 0,1,2,3,4,5,6,7,8,9,}; // List\u0026lt;Point\u0026gt; values = new List\u0026lt;Point\u0026gt;(Zip(xValues, yValues, (x, y) =\u0026gt; new Point(x, y))); private static IEnumerable\u0026lt;TOutput\u0026gt; Zip\u0026lt;T1, T2, TOutput\u0026gt;(IEnumerable\u0026lt;T1\u0026gt; left, IEnumerable\u0026lt;T2\u0026gt; right, Func\u0026lt;T1, T2, TOutput\u0026gt; generator) { IEnumerator\u0026lt;T1\u0026gt; leftEnumerator = left.GetEnumerator(); IEnumerator\u0026lt;T2\u0026gt; rightEnumerator = right.GetEnumerator(); while (leftEnumerator.MoveNext() \u0026amp;\u0026amp; rightEnumerator.MoveNext()) { yield return generator(leftEnumerator.Current, rightEnumerator.Current); } leftEnumerator.Dispose(); rightEnumerator.Dispose(); } private class Point { public double X { get; set; } public double Y { get; set; } public Point(double x, double y) { X = x; Y = y; } public Point(TextReader textReader) { } } /// InputCollection\u0026lt;Point\u0026gt; points = new InputCollection\u0026lt;Point\u0026gt;((InputStream) =\u0026gt; new Point(InputStream)); public delegate T CreateFromStream\u0026lt;T\u0026gt;(System.IO.TextReader textReader); public class InputCollection\u0026lt;T\u0026gt; { private List\u0026lt;T\u0026gt; thingsRead = new List\u0026lt;T\u0026gt;(); private readonly CreateFromStream\u0026lt;T\u0026gt; readFunc; public InputCollection(CreateFromStream\u0026lt;T\u0026gt; readFunc) { this.readFunc = readFunc; } public void ReadFromStream(System.IO.TextReader textReader) { thingsRead.Add(readFunc(textReader)); } public IEnumerable\u0026lt;T\u0026gt; Values =\u0026gt; thingsRead; } 做法 23 心得 可以利用 這種方式，讓不同類別擁有相同介面，使用相同的方法，讓程式彈性變高\n// ForeachIn(items,(item, i) =\u0026gt; // { // item.SetIcon(icon[i]); // }); // ForeachIn(items,(item, i) =\u0026gt; // { // item.SetPosition(positions[i]); // }); public void ForeachIn\u0026lt;T\u0026gt;(T[] arr, Action\u0026lt;T, int\u0026gt; action) { for (int i = 0; i \u0026lt; arr.Length; i++) { action(arr[i], i); } } ","date":"2024-10-13T03:11:00+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-22-23/","title":"Effective C# 做法 22-23"},{"content":"做法 20 以 IComparable\u0026lt;T\u0026gt; 與 IComparer\u0026lt;T\u0026gt; 實作排序關係\n實作 public struct Customer : IComparable\u0026lt;Customer\u0026gt;, IComparable { private readonly string name; private double revenue; public Customer(string name) { this.name = name; } public int CompareTo(Customer other) { return name.CompareTo(other.name); } int IComparable.CompareTo(object? obj) { if (obj is Customer other) { return this.CompareTo(other); } throw new ArgumentException(\u0026#34;Argument must be a Customer\u0026#34;, nameof(obj)); } public static bool operator \u0026lt;(Customer left, Customer right) =\u0026gt; left.CompareTo(right) \u0026lt; 0; public static bool operator \u0026lt;=(Customer left, Customer right) =\u0026gt; left.CompareTo(right) \u0026lt;= 0; public static bool operator \u0026gt;(Customer left, Customer right) =\u0026gt; left.CompareTo(right) \u0026gt; 0; public static bool operator \u0026gt;=(Customer left, Customer right) =\u0026gt; left.CompareTo(right) \u0026gt;= 0; private static Lazy\u0026lt;RevenueComparer\u0026gt; revComp = new Lazy\u0026lt;RevenueComparer\u0026gt;(() =\u0026gt; new()); public static IComparer\u0026lt;Customer\u0026gt; RevenueCompare =\u0026gt; revComp.Value; public static Comparison\u0026lt;Customer\u0026gt; CompareByRevenue =\u0026gt; (left, right) =\u0026gt; left.revenue.CompareTo(right.revenue); private class RevenueComparer : IComparer\u0026lt;Customer\u0026gt; { public int Compare(Customer left, Customer right) { return left.revenue.CompareTo(right.revenue); } } } 使用方式 static void Main(string[] args) { // 創建一些 Customer 實例 var customers = new List\u0026lt;Customer\u0026gt; { new Customer(\u0026#34;Alice\u0026#34;,1000) , new Customer(\u0026#34;Bob\u0026#34;,1500) , new Customer(\u0026#34;Charlie\u0026#34;,1200), new Customer(\u0026#34;David\u0026#34;,800) }; // 1. 使用默認比較（按名稱） customers.Sort(); Console.WriteLine(\u0026#34;Sorted by name:\u0026#34;); // Alice: 1000 // Bob: 1500 // Charlie: 1200 // David: 800 PrintCustomers(customers); // 2. 使用運算符比較 var alice = new Customer(\u0026#34;Alice\u0026#34;); var bob = new Customer(\u0026#34;Bob\u0026#34;); // Alice \u0026lt; Bob: True // Alice \u0026lt;= Bob: True // Alice \u0026gt; Bob: False // Alice \u0026gt;= Bob: False Console.WriteLine($\u0026#34;Alice \u0026lt; Bob: {alice \u0026lt; bob}\u0026#34;); Console.WriteLine($\u0026#34;Alice \u0026lt;= Bob: {alice \u0026lt;= bob}\u0026#34;); Console.WriteLine($\u0026#34;Alice \u0026gt; Bob: {alice \u0026gt; bob}\u0026#34;); Console.WriteLine($\u0026#34;Alice \u0026gt;= Bob: {alice \u0026gt;= bob}\u0026#34;); // 3. 使用 RevenueCompare customers.Sort(Customer.RevenueCompare); Console.WriteLine(\u0026#34;\\nSorted by revenue (using RevenueCompare):\u0026#34;); // David: 800 // Alice: 1000 // Charlie: 1200 // Bob: 1500 PrintCustomers(customers); // 4. 使用 CompareByRevenue customers.Sort(Customer.CompareByRevenue); Console.WriteLine(\u0026#34;\\nSorted by revenue (using CompareByRevenue):\u0026#34;); // David: 800 // Alice: 1000 // Charlie: 1200 // Bob: 1500 PrintCustomers(customers); // 5. 使用 LINQ 排序 var sortedByName = customers.OrderBy(c =\u0026gt; c); Console.WriteLine(\u0026#34;\\nSorted by name (using LINQ):\u0026#34;); // lice: 1000 // Bob: 1500 // Charlie: 1200 // David: 800 PrintCustomers(sortedByName); var sortedByRevenue = customers.OrderBy(c =\u0026gt; c, Customer.RevenueCompare); Console.WriteLine(\u0026#34;\\nSorted by revenue (using LINQ and RevenueCompare):\u0026#34;); // David: 800 // Alice: 1000 // Charlie: 1200 // Bob: 1500 PrintCustomers(sortedByRevenue); } static void PrintCustomers(IEnumerable\u0026lt;Customer\u0026gt; customers) { foreach (var customer in customers) { Console.WriteLine($\u0026#34;{customer.ToString()}\u0026#34;); } } 做法 21 建構支援 Disposable 型別參數的泛型類別\n如果你建構的泛型類別的型別參數描述的任何型別實例，你必須考慮到這些型別可能有實作 IDisposable。你必須防衛性的撰寫程式並確保這些物件離開範圍後不會洩漏資源。\npublic interface IEngine { void DoWork(); } private class Engine : IEngine { public void DoWork() { Console.WriteLine(\u0026#34;Hello World!\u0026#34;); } } public class EngineDriver1\u0026lt;T\u0026gt; where T : IEngine, new() { public void GetThingsDone() { T engine = new T(); using (engine as IDisposable) { engine.DoWork(); } } } // using (var x = new EngineDriver2\u0026lt;Engine\u0026gt;()) // { // x.GetThingsDone(); // } public class EngineDriver2\u0026lt;T\u0026gt; : IDisposable where T : IEngine, new() { private Lazy\u0026lt;T\u0026gt; engine = new Lazy\u0026lt;T\u0026gt;(() =\u0026gt; new T()); public void GetThingsDone() { using (engine.Value as IDisposable) { engine.Value.DoWork(); } } public void Dispose() { if (engine.IsValueCreated) { var resource = engine.Value as IDisposable; resource?.Dispose(); } } } public class EngineDriver3\u0026lt;T\u0026gt; where T : IEngine { private T engine; public EngineDriver3(T engine) { this.engine = engine; } public void GetThingsDone() { engine.DoWork(); } } 若 T 沒有實作 IDisposable ，則此區域變數的值為 null 如果 T（在這個例子中是 Engine）沒有實作 IDisposable 接口，那麼 engine as IDisposable 的結果就會是 null。 這個 null 值並不會導致異常或錯誤。 using 語句被設計為可以安全地處理 null 值。當 disposable 為 null 時， using 語句簡單地不執行任何釋放資源的操作。 這就是為什麼即使 Engine 類沒有實作 IDisposable，代碼仍然可以正常運行。 using 語句在這種情況下實際上不會做任何特殊的事情，代碼會繼續執行 engine.DoWork()。\n","date":"2024-10-07T00:57:47+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-20-21/","title":"Effective C# 做法 20-21"},{"content":"做法 18 使用泛型 \u0026amp; 定義最少與足夠的約束\n使用泛型 為什麼這三組執行期都共用相同的程式\nList\u0026lt;string\u0026gt; strings = new List\u0026lt;string\u0026gt;(); List\u0026lt;Stream\u0026gt; streams=newList\u0026lt;Stream\u0026gt;(); List\u0026lt;MyClassType\u0026gt; myClassTypes=newList\u0026lt;MyClassType\u0026gt;(); JIT 編譯器識別出這些類型都是引用類型，因此可以使用相同的機器代碼。\n為什麼這三組有不同的機械碼?\nList\u0026lt;double\u0026gt; doubles = new List\u0026lt;double\u0026gt;(); List\u0026lt;int\u0026gt; ints = new List\u0026lt;int\u0026gt;(); List\u0026lt;MyStruct\u0026gt; myStructs = new List\u0026lt;MyStruct\u0026gt;(); CLR 為每種值類型參數生成特定的代碼，以優化性能和類型安全性。\n1. 泛型類別使用 default() 在講到泛型類別使用 default() 的方法寫時，需要把 T 改成 T?，因為 Value Type 的 Default = 0，而 Reference Type 是沒有 Default，回傳的結果會是 null。 需要告訴使用者可能會回傳 null 建議還是要回傳 T?，也提高程式碼閱讀性。\npublic static T? FirstOrDefault\u0026lt;T\u0026gt;(this IEnumerable\u0026lt;T\u0026gt; source, Predicate\u0026lt;T\u0026gt; predicate) { foreach (var item in source) { if (predicate(item)) return item; } return default; } 2. 少了 null 確認 原本課本的判斷，少了 null 確認\npublic static bool AreEqual(T left, T right) =\u0026gt; left.Equals(right); 可以改這種寫法，多了做法 03 的寫法，用來判斷 null。\npublic static bool AreEqual\u0026lt;T\u0026gt;(T left, T right) { if (left is T qu) { return qu.Equals(right); } return right == null; } 3. 使用 T? 使用 T? 明確表示 factory 方法可能返回 null。這增加了代碼的可讀性和意圖的清晰度。\nMyClass obj1 = Factory(() =\u0026gt; new MyClass { Name = \u0026#34;Object 1\u0026#34; }); Console.WriteLine(obj1.Name); // 輸出: Object 1 // 使用返回 null 的工廠方法 MyClass obj2 = Factory\u0026lt;MyClass\u0026gt;(() =\u0026gt; null); Console.WriteLine(obj2.Name); // 輸出: Default Name public delegate T FactoryFunc\u0026lt;T\u0026gt;(); public static T Factory\u0026lt;T\u0026gt;(FactoryFunc\u0026lt;T?\u0026gt; factory) where T : new() { T? resultVal = factory(); if (resultVal == null) { resultVal = new T(); } return resultVal; } 4. 為什麼要盡量避免約束 new()、struct、class new() 約束 優點：\n保證類型有無參數構造函數。 缺點： 限制了類型的靈活性，排除了沒有公共無參數構造函數的類型。 可能導致不必要的對象創建，影響性能。 難以進行單元測試，因為無法輕易模擬或替換對象。 struct 約束 優點：\n確保類型是值類型。 缺點：\n過度限制了類型，排除了可能同樣適用的引用類型。 可能導致不必要的裝箱和拆箱操作。 限制了代碼的重用性和靈活性。 class 約束 優點：\n確保類型是引用類型。 缺點：\n排除了可能同樣適用的值類型。 限制了代碼的通用性。 可能導致不必要的堆分配。 一般建議：\n優先使用接口約束：接口約束更靈活，能同時適用於值類型和引用類型。例如：\npublic class GenericClass\u0026lt;T\u0026gt; where T : IComparable\u0026lt;T\u0026gt; 基於行為而非類型種類進行約束：關注類型能做什麼，而不是類型是什麼。\n考慮使用泛型約束的組合：在必要時，可以組合多個接口約束來精確定義所需的功能。\n使用 default 關鍵字：代替 new()，使用 default(T) 來獲取類型的默認值。\n設計靈活的 API：避免過度約束，以增加代碼的重用性和適應性。\n考慮性能影響：某些約束（如 struct）可能導致意外的性能問題。\n總結 避免使用 new()、 struct 和 class 約束主要是為了提高代碼的靈活性、可重用性和可測試性。通過使用更通用的約束（如接口），我們可以編寫出更加靈活和強大的泛型代碼，適用於更廣泛的類型。\n做法 19 使用執行期型別檢查特化泛型演算法 實作\npublic sealed class ReverseEnumerable\u0026lt;T\u0026gt; : IEnumerable\u0026lt;T\u0026gt; { private class ReverseEnumerator : IEnumerator\u0026lt;T\u0026gt; { private int currentIndex = 0; private IList\u0026lt;T\u0026gt; collection; public T Current =\u0026gt; collection[currentIndex]; object IEnumerator.Current =\u0026gt; Current; public ReverseEnumerator(List\u0026lt;T\u0026gt; collection) : this(collection.ToArray()) { } public ReverseEnumerator(IList\u0026lt;T\u0026gt; collection) { currentIndex = collection.Count; this.collection = collection; } public void Dispose() { } public bool MoveNext() { return --currentIndex \u0026gt;= 0; } public void Reset() { currentIndex = collection.Count; } } private sealed class ReverseStringEnumerator : IEnumerator\u0026lt;char\u0026gt; { private string sourceSequence; private int currentIndex; public ReverseStringEnumerator(string source) { sourceSequence = source; currentIndex = source.Length; } public char Current =\u0026gt; sourceSequence[currentIndex]; object IEnumerator.Current =\u0026gt; sourceSequence[currentIndex]; public void Dispose() { } public bool MoveNext() { return --currentIndex \u0026gt;= 0; } public void Reset() { currentIndex = sourceSequence.Length; } } private IEnumerable\u0026lt;T\u0026gt;? sourceSequence; private IList\u0026lt;T\u0026gt;? originalSequence; public ReverseEnumerable(IEnumerable\u0026lt;T\u0026gt; source) { if (source is ReverseEnumerable\u0026lt;T\u0026gt; reverseEnumerable) sourceSequence = reverseEnumerable; if (source is string str) { originalSequence = str.ToArray() as IList\u0026lt;T\u0026gt;; } else { originalSequence = source as IList\u0026lt;T\u0026gt;; } } public ReverseEnumerable(IList\u0026lt;T\u0026gt; source) : this((IEnumerable\u0026lt;T\u0026gt;)source) { } public IEnumerator\u0026lt;T\u0026gt; GetEnumerator() { if (sourceSequence is string str) { return new ReverseStringEnumerator(str) as IEnumerator\u0026lt;T\u0026gt;; } if (originalSequence == null) { if (sourceSequence is ICollection\u0026lt;T\u0026gt; source) { originalSequence = new List\u0026lt;T\u0026gt;(source.Count); } else { originalSequence = new List\u0026lt;T\u0026gt;(sourceSequence); } } return new ReverseEnumerator(originalSequence); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } 使用方式\nprivate static void StringEnumerable() { string str = \u0026#34;Hello, World!\u0026#34;; var reverseEnumerable = new ReverseEnumerable\u0026lt;char\u0026gt;(str); foreach (char c in reverseEnumerable) { Console.Write(c); } /// !dlroW ,olleH Console.WriteLine(); } private static void ListEnumerable() { List\u0026lt;int\u0026gt; list = new List\u0026lt;int\u0026gt;() { 1, 2, 3, 4, 5, 6, 7 }; var reverseEnumerable = new ReverseEnumerable\u0026lt;int\u0026gt;(list); foreach (int i in reverseEnumerable) { Console.Write(i); } /// 76543210 Console.WriteLine(); } ","date":"2024-09-28T01:48:09+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-18-19/","title":"Effective C# 做法 18-19"},{"content":"做法 16 絕不在建構元中呼叫虛擬函式\n本章節有提到 Static Code Analyzer 工具，可以利用這些工具避免建構子中呼叫虛擬函式。 工具分別有\nVisual Studio JetBrains Rider Visual Studio + ReSharper 針對 Unity 的 Static Code Analyzer 只能用這些工具 JetBrains Rider Visual Studio + ReSharper Unity + Roslyn 做法 17 `實作標準 Dispose 模式\nUnmanaged 型別 sbyte、byte、short、ushort、int、uint、long、ulong、nint、nuint、char、float、double、decimal 或 bool\n任何 enum 型別\n任何指標型別\nTuple 其成員皆為非受控型別\n任何只包含非受控型別欄位的使用者定義結構型別。\nUnmanaged 資源 是否繼承 IDisposable 檢查類別文件 分析類別用途和功能 看原始碼 利用反射，查看有沒有 Finalizer，如 sadehandle 需要實現 IDisposable 的類別 有 Unmanaged 資源 包含 IDisposable 成員 大型物件或耗資源的類別，如大型資料庫、影片、音效等 長生命週期物件，如連線功能、控制器等 自訂義資源管理類別 不需要實現 IDisposable 的類別 純數據類資料 無狀態工具類 短生命週期的簡單物件 不管理任何資源的類別 不是每的類別都蓄要繼承 IDisposable 。僅當你的類別管理 Unmanaged 資源或者包含實現 IDisposable 介面的成員，才考慮繼承 IDisposable。 1.除非你的類別直接持有 Unmanaged 資源，否則你不應該實作 finalizer 實作 Dispose 方法\n","date":"2024-09-22T22:17:41+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-16-17/","title":"Effective C# 做法 16-17"},{"content":"做法 14 減少重複的初始化邏輯\n1. 建構子初始化程序讓一個建構子呼叫其他的建構子 public class MyClass { private List\u0026lt;string\u0026gt; coll; private string name; public MyClass() : this(0, string.Empty) { } public MyClass(int count, string name) { coll = new List\u0026lt;string\u0026gt;(count); this.name = name; } } 2. 選擇預設參數與 選擇預設參數與使用多個建構子多載之間需要權衡考量，建議是不超過 3 個，超過時可以考慮使用一個類別當初始化類別使用。一般來說應該偏好預設值而非多載建構子。\n3. 繼承類別不能修改父類別中宣告為 readonly 的欄位 readonly 欄位只能在宣告時或在定義該欄位的類別建構子中賦值。\n做法 15 避免建構不必要的物件\nprotected override void OnPaint(PaintEventArgs e) { /// 劣 using(Font myFont = new Font(\u0026#34;Arial\u0026#34;, 12.0f)){ e.Graphics.DrawString(\u0026#34;Hello, World!\u0026#34;, myFont, Brushes.Black, 10, 10); } } 推薦方式\nprivate readonly Font myFont = new Font(\u0026#34;Arial\u0026#34;, 12.0f); protected override void OnPaint(PaintEventArgs e) { e.Graphics.DrawString(\u0026#34;Hello, World!\u0026#34;, myFont, Brushes.Black, 10, 10); } 1. 區域變數的選擇 在區域變數是參考型別（實值類型就沒關係），且會在經常被呼叫的程序中使用時將它提升至成員變數。\n2. string 不可變的原因 安全性：敏感資訊不會因此被修改。 執行緒安全：不可變性是天然的執行緒安全。 hash 一致：字典的 key 相同內容、相同的位址。 字串池：.NET 優化 性能優化：編譯器和運行可優化字串。 API 設計簡化 3.建構不可變的可變 builder 類別 可以參考設計模式中的建造者模式。\n","date":"2024-09-14T23:57:56+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-14-15/","title":"Effective C# 做法 14-15"},{"content":"做法 11 認識 .NET 資源管理\n1. 簡易圖示記憶體區塊的關聯圖 2. 記憶體排列物件的放置位址 呼叫 GC 後，記憶體位置改變了，這就是記憶體回收的概念\n3. 記憶體層代 層代 0\n最新的層代而且包含存留較短的物件。大部分物件都會在層代 0 的記憶體回收，而且不會存留至下一個層代。記憶體回收從第 0 個層代開始。\n層代 1\n這個層代包含存留較短的物件，且當做較短與較長物件的緩衝區。層代 0 回收後，將壓縮可取得的物件，升階至層代 1。\n層代 0 已滿時，記憶體會執行回收。如果層代 0 回收沒有足夠的記憶體，才會執行層代 1 回收，再執行層代 2 回收，層代 1 回收之後有存留的物件，會提升至層代 2。\n層代 2\n存留較長的物件。層代 2 回收存留的物件還是回留在層代 2。\n最後結論就是，假如不在一開始就把物件釋放掉，最後存留至層代 1、2 時，要回收物件的時間就會越來越久，造成的記憶體效率不佳，所以要手動釋放掉物件。\n記憶體回收的基本概念\n4. 使用 Dispose 釋放資源，而不是 Finalizer 做法 12 偏好成員初始化程序而非指派陳述\n為什麼 initobj 會導致 box、unbox？ initobj 指令用於將值類型的變量初始化為其默認值，因此 initobj 不會導致 boxing 或 unboxing。 使用 initobj 時，是直接記憶體操作，不涉及對象的創建或類型的轉換。\n推薦用法\npublic class MyClass { private List\u0026lt;string\u0026gt; labels = new List\u0026lt;string\u0026gt;(); } 做法 13 對靜態類別成員進行適當的初始化\nSingleton 初始化具有複雜的邏輯，可以在 static 建構子中寫。\npublic class MySingleton { private static readonly MySingleton instance = new MySingleton(); public static MySingleton Instance =\u0026gt; instance; private MySingleton() { } } public class MySingleton2 { private static readonly MySingleton2 instance; static MySingleton2() { instance = new MySingleton2(); } public static MySingleton2 Instance =\u0026gt; instance; private MySingleton2() { } } ","date":"2024-09-07T13:54:06+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-11-13/","title":"Effective C# 做法 11-13"},{"content":"做法 07 以 delegate 表示 callback\n可以利用 delegate 回傳 bool 當判斷或者是回傳類別等功能降低城市耦合。\nList\u0026lt;int\u0026gt; numbers = Enumerable.Range(1, 10).ToList(); var oddNumbers = numbers.Find(x =\u0026gt; x % 2 == 1); var test = numbers.TrueForAll(x =\u0026gt; x \u0026lt; 50); numbers.RemoveAll(x =\u0026gt; x % 2 == 0); numbers.ForEach(x =\u0026gt; Console.WriteLine(x)); public void LengthyOperation2(Func\u0026lt;bool\u0026gt; pred) { bool bContinue = true; foreach (var cl in container) { cl.DoLengthyOperation(); foreach (Func\u0026lt;bool\u0026gt; pr in pred.GetInvocationList()) bContinue \u0026amp;= pr(); if (!bContinue) return; } } 做法 08 對事件叫用使用空條件運算子\n推薦這樣寫\npublic void RaiseUpdate() { counter++; Updated?.Invoke(this, counter); } 做法 09 減少 boxing 與 unboxing\nint i = 25; object o = i; /// box Console.WriteLine(o.ToString()); /// 25 object fp = 5; object fo = fp; int ip = (int)fo; /// unbox Console.WriteLine(ip.ToString()); /// 5 實值類型可變轉換成 System.Object 或任何界面參考。這些轉換隱含的發生，使得尋找它們變得更為複雜。 boxing 與 unboxing 操作會在你預料之外的複製拷貝，這導致 bug。以多型方式處理實值類型也有效能成本。\n參考類型轉換為 System.Object 或介面時的行為確實不同：\n參考類型轉換: 參考類型本來就是從 System.Object 繼承的，所以轉換為 System.Object 或實現的介面時不需要 boxing。 這種轉換只是改變了引用的類型，不會創建新的對象或複製數據。 效能影響: 參考類型轉換為 System.Object 或介面基本上沒有額外的效能成本。 不會發生像實值類型那樣的額外記憶體分配或數據複製。 多態性: 參考類型本來就支持多態，不需要額外的轉換步驟。 潛在問題: 雖然參考類型不會遇到 boxing/unboxing 的問題，但可能會遇到其他問題，如意外的類型轉換或空引用。 做法 10 只對基底類別更新使用 new 修飾詞\npublic class MyClass { public new void MagicMethod() { } } new 修飾詞必須小心使用。如果你不假思索的套用它，你會在你的物件中產生模糊的方法呼叫。這是發生在升級你的基底類別導致與你的類別衝突的特殊情況。就算在這種情況下，使用前還是要仔細思考。更重要的是，其他情況不要使用它。\n","date":"2024-08-28T00:19:53+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-07-10/","title":"Effective C# 做法 07-10"},{"content":"做法 04 以內插字串取代 string.Format()\n推薦這樣寫\nstring name = \u0026#34;test\u0026#34;; float height = 180; int age = 20; string str = $\u0026#34;userName: {name}, age: {age}, height: {height}\u0026#34;; 1. 為什麼 Math.PI 不寫 ToString 會造成 box 呢? 當需要調用 ToString() 方法時，如果我們沒有顯式調用，編譯器會嘗試使用 Object.ToString()。但是，為了使用 Object 上的方法，值類型必須先轉換為 Object，這就導致了裝箱。\n做法 05 對文化特定字串偏好 FormattableString\n推薦使用 FormattableString，不會因為當地時間顯示或者數字的顯示有所差異。double 的小數點會是＂.＂；如果再歐洲會是顯示＂,＂。\n做法 06 避免字串型別 API\n使用 nameof 運算子時，任何對於屬性名稱的改變都會正確的反應在用於事件參數的字串中。\npublic string Name { get { return name; } set { if (value != name) { name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } } ","date":"2024-08-22T21:28:59+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-04-06/","title":"Effective C# 做法 04-06"},{"content":"做法 01 偏好隱含型別的區域變數\n參考書籍提到的最後一段\n簡單說，除非開者（包括以後的你）必須看到型別宣告才能理解程式，否則就使用 var 宣告區域變數。這個做法的標題是＂偏好＂而不是＂總是＂。我建議明確的宣告所有數值型別（int、float、double 與其他）而不要使用 var 宣告。其他東西就是用 var。多打幾個字 - 明確的宣告型別 - 不會提升行別安全或改善可讀性。如果挑錯宣告型別，你可能會造成編譯器本來能夠避免的低效率。\n做法 02 偏好 readonly 而非 const\n必須在編譯期確定的值必須使用 const：屬性參數、switch case 標籤與 enum 定義，以及少數部會在版本間變化的數值。其餘狀況則傾向以 readonly 常數提升彈性。\n1. const 使用建議 對於在編譯時就能確定且永不改變的值 適用於基本數據類型（int、float、bool 等）和字符串 效能略優於 readonly，因為它是編譯時常量 2. readonly 使用建議 對於運行時才能確定值的情況 可用於任何數據類型，包括引用類型和複雜類型 允許在構造函數中賦值 選擇指南 如果值在編譯時就能確定，且是基本類型或字符串，優先使用 const。 如果是引用類型或需要在運行時計算的值，使用 readonly。 如果需要在不同的構造函數中賦予不同的值，使用 readonly。 對於靜態成員，如果符合 const 的條件，優先使用 const；否則使用 static readonly。 做法 03 偏好 is 或 as 運算子而非型別轉換\n推薦這樣寫\nobject o = new MyType(); /// 不推薦 var t = (MyType)o; /// 推薦 if (o is MyType myType) { myType.Do(); /// do samething ... } ","date":"2024-08-17T23:09:29+08:00","permalink":"/posts/effective-c%23-%E5%81%9A%E6%B3%95-01-03/","title":"Effective C# 做法 01-03"},{"content":"前言 之前有講到 C# Value Type、Reference Type 的差異，現在來講一下淺複製（Shallow Copy）與深複製（Deep Copy）。\n淺複製 將原有物件的欄位依照其型別來複製，Value Type 欄位複製其數值到另一個空間，Reference Type 欄位則是複製其參考到另一個空間，因此此兩物件的 Reference Type 都是參考到同一 instance。\n淺複製方式 public class Address { public string Street = string.Empty; public string City = string.Empty; } public class Person { public string Name = string.Empty; public int Age = 0; public Address Address = new Address(); public Person ShallowCopy() { return (Person)this.MemberwiseClone(); } } Person origin = new Person() { Name = \u0026#34;Sam\u0026#34;, Age = 20, Address = new Address() { Street = \u0026#34;123 Main St\u0026#34;, City = \u0026#34;Anytown\u0026#34; } }; Person copy = origin.ShallowCopy(); copy.Name = \u0026#34;John\u0026#34;; copy.Age = 30; copy.Address.Street = \u0026#34;456 Main St\u0026#34;; copy.Address.City = \u0026#34;Taiwan\u0026#34;; Console.WriteLine(\u0026#34;Shallow Copy\u0026#34;); // Origin: Sam, 20, 456 Main St, Taiwan Console.WriteLine($\u0026#34;Origin: {origin.Name}, {origin.Age}, {origin.Address.Street}, {origin.Address.City}\u0026#34;); // Copy: John, 30, 456 Main St, Taiwan Console.WriteLine($\u0026#34;Copy: {copy.Name}, {copy.Age}, {copy.Address.Street}, {copy.Address.City}\u0026#34;); 輸出結果\nOrigin: Sam, 20, 456 Main St, Taiwan\nCopy: John, 30, 456 Main St, Taiwan\nName 是 String，可是沒有跟著改變，可以先看C# Reference Type String這篇。\nAge 是 int，也是 Value Type，所以會改變。\nAddress 是 class 是 Reference Type，因為淺複製的關係導致他們（Origin、Copy）有一起改變\n深複製 ValueType 與淺複製相同，主要差異是深複製的 Reference Type 欄位會產生新的 instance，所以其倆欄位是參考到不同 instance 的。\n深複製方式 public class Address { public string Street = string.Empty; public string City = string.Empty; } public class Person { public string Name = string.Empty; public int Age = 0; public Address Address = new Address(); public Person DeepCopy() { Person clone = (Person)this.MemberwiseClone(); clone.Address = new Address { Street = this.Address.Street, City = this.Address.City }; return clone; } } Person origin = new Person() { Name = \u0026#34;Sam\u0026#34;, Age = 20, Address = new Address() { Street = \u0026#34;123 Main St\u0026#34;, City = \u0026#34;Anytown\u0026#34; } }; Person copy = origin.DeepCopy(); copy.Name = \u0026#34;John\u0026#34;; copy.Age = 30; copy.Address.Street = \u0026#34;456 Main St\u0026#34;; copy.Address.City = \u0026#34;Taiwan\u0026#34;; Console.WriteLine(\u0026#34;Shallow Copy\u0026#34;); // Origin: Sam, 20, 123 Main St, Anytown Console.WriteLine($\u0026#34;Origin: {origin.Name}, {origin.Age}, {origin.Address.Street}, {origin.Address.City}\u0026#34;); // Copy: John, 30, 456 Main St, Taiwan Console.WriteLine($\u0026#34;Copy: {copy.Name}, {copy.Age}, {copy.Address.Street}, {copy.Address.City}\u0026#34;); Origin: Sam, 20, 123 Main St, Anytown\nCopy: John, 30, 456 Main St, Taiwan\n與淺複製的差異，深複製的 Address 欄位也會產生新的 instance，在修改 copy.Address 時，不會影響到 origin.Address。\n參考連結 C# 複製物件的方式比較\n","date":"2024-06-22T19:02:04+08:00","permalink":"/posts/c%23-%E6%B7%BA%E8%A4%87%E8%A3%BD%E8%88%87%E6%B7%B1%E8%A4%87%E8%A3%BD/","title":"C# 淺複製與深複製"},{"content":"前言 之前有提到 C# Value Type、Reference Type 的差異，提到 String 是 Reference Type，可是在使用時卻很像 Value Type。\n比較值 Value Type 以 int 為舉例\nint a = 1; int b = a; a = 2; Console.WriteLine(a == b); // 輸出: false b 並沒有隨著 a 改變，看得出 int 是 Value Type。\nstring str1 = \u0026#34;abc\u0026#34;; string str2 = str1; str1 = \u0026#34;def\u0026#34;; Console.WriteLine(str1 == str2); // 輸出: false 修改 str1 卻沒有影響到 str2，所以會覺得 string 也是 Value Type(?)。\n比較地址 使用 object.ReferenceEquals 來比較，假如是 Value Type，則會輸出 false。\nint a = 1; int b = 1; Console.WriteLine(object.ReferenceEquals(a, b)); // 輸出: false int 確實是 Value Type，所以會輸出 false。\nstring str1 = \u0026#34;abc\u0026#34;; string str2 = \u0026#34;abc\u0026#34;; Console.WriteLine(object.ReferenceEquals(str1, str2)); // 輸出: true 比較結果為 true，假如 String 是 Value Type，應該會與 int 的結果一樣會是 false。\n也就是 String 其實是 Reference Type。\nString 為什麼是 Reference Type .NET 框架-string 是 value or reference type?\n這裡面提到兩點\nString 對象，若值相同，則其引用地址相同。\nString 對象，若值不等，則其引用地址不等。\nstring str1 = \u0026#34;abc\u0026#34;; //str1指向記憶體位置 addressA 為 abc string str2 = str1; //str2指向記憶體位置 addressA str1 = \u0026#34;def\u0026#34;; //str1新指向記憶體位置 addressB 為 def Console.WriteLine(str1 == str2); // 輸出: false String 特點 String 特點就是具有不可變性（immutable），一旦 new String 在記憶體(managed heap)上為它分配一塊連續記憶體空間，我們將不能以任何方式對這個 String 進行修改。所有對這個 String 進行各項操作而返回的 String，實際上是另一個重新 new 的 String，其本身並不會產生任何變化。\nString 效能如何？ 從上面就可以知道 String 有不可變性，一旦創建了就不能修改值，每次修改 String 都會產生一個新的 String，所以 String 效能比較低。\n所以需要經常性操作 String 可以考慮使用 StringBuilder。\n結論 來自 ChatGPT 的解釋\nC# String 有以下特性：\nString 有不可變性：一旦創建 string，它的內容就不能被改變。\n賦值操作的實際行為：當執行 str = \u0026quot;abc\u0026quot; 時，實際上是創建了一個新的 string 對象，而不是修改原有的對象。然後，變量 str 被重新指向這個新對象。\n為什麼 str2 不變：當執行 string str2 = str1 時，str1 和 str2 確實指向了同一個對象。但是當 str1 被賦予新值時，它指向了一個新對象，而 str2 仍然指向原來的對象。\n值類型的行為：這種行為看起來很像值類型，但 string 仍然是引用類型。這是因為 string 的不可變性和特殊的內存管理方式。\n性能和內存管理：這種設計有助於提高性能和簡化內存管理，特別是在字符串被廣泛使用的情況下。\n字符串池（String Interning）：C#使用字符串池來優化內存使用。相同的字符串字面量會指向內存中的同一位置。\n這種行為是 C# 語言設計的一個特點，旨在結合引用類型的靈活性和值類型的一些優勢。它可能看起來有點反直覺，但在實際使用中通常是有益的。\n","date":"2024-06-22T17:40:52+08:00","permalink":"/posts/c%23-reference-type-string/","title":"C# Reference Type String"},{"content":"前言 之前有講到 C# Value Type、Reference Type 的差異，現在來講一下 Class 和 Struct 選擇。\n從上一篇的文章中可以知道 Class 是 Reference type，而 Struct 是 Value type。\n如何選擇 根據在類別和結構之間選擇\n作為經驗規則，架構中大部分的類型應該為類別。 不過，在某些情況下，實值型別的特性會使它更適合使用結構。\n可以知道其實大部分時都是使用 Class，但在某些情況下使用 Struct 會比較適合。\n什麼時候使用 Struct 如果類型的執行個體很小，且通常短期或通常內嵌在其他物件中，請考慮定義結構，而不是類別。\n除非類型具有下列所有特性，否則請「避免」定義結構：\n其以邏輯方式表示單一值，類似於基本類型 (int、double 等)。\n其執行個體大小低於 16 個位元組。\n類型為不可變，\n而且不需要經常進行 Box。\n在所有其他情況下，您應該將類型定義為類別。\n參考連結 一起學 Class and Struct (C#)\n","date":"2024-06-12T22:47:08+08:00","permalink":"/posts/c%23-class-%E5%92%8C-struct-%E9%81%B8%E6%93%87/","title":"C# Class 和 Struct 選擇"},{"content":"前言 在公司讀書會與同事討論淺複製與深複製的差異和使用時機時，聊到 Reference Type、Value Type 的不同，但是要講淺複製與深複製之前要先講 Reference Type、Value Type 的差異。\nValue Type 有哪些 整數的數字型別：sbyte、byte、short、ushort、int、uint、long、ulong、nint、nuint\n浮點數值型別：float、double、decimal\n內建數值轉換\nbool\nchar\nenum\nstruct\nref struct\ntuples\n可為 Null 的值類型 (C# 參考)\n參考：實值類型 (C# 參考)\nReference Type 有哪些 宣告參考型別：class、interface、delegate、record\n內建參考類型：dynamic、object、string\n可為 Null 的參考型別\n集合和陣列：集合（List、Dictionary）、陣列（int[]、string[]）\n參考：參考型別 (C# 參考)\nValue Type、Reference Type 的差異 根據在類別和結構之間選擇裡的一段話，可以分出 5 點差異。\n1. 存放記憶體 參考型別會配置在回收的堆積和記憶體上，而實值型別則會配置在堆疊上，或內嵌在包含型別上，並在堆疊回溯時或其包含類型解除配置時解除配置。\n因此，實值型別的配置和解除配置的成本通常比參考型別的配置和解除配置的成本更低。\n可以得知主要是在存放的記憶體差異，因此 Value Type 配置和解除配置的成本比 Reference Type 成本更低。\n實值型別的配置和解除配置的成本通常比參考型別的配置和解除配置的成本更低。\n2. 陣列 參考型別的陣列會以換行方式配置，這表示陣列元素只是位於堆積上之參考型別執行個體的參考。實值型別陣列會內嵌配置，這表示陣列元素是實值型別實際的執行個體。\n因此，實值型別陣列的配置和解除配置的成本會遠比參考型別陣列的配置和解除配置的成本來得低。此外，在大部分情況下，實值型別陣列會呈現較佳的參考位置。\nValue Type 陣列的配置和解除配置成本較低，且通常在記憶體存取表現上優於 Reference Type 陣列。然而，Reference Type 陣列在處理複雜的物件或需要物件參考的情境中仍有其必要性。\n3. 記憶體使用量 強制轉型為參考型別或其實作的其中一個介面時，實值型別會進行 Boxed。 當強制轉型回實值型別時，它們會進行 Unboxed。\n因為 Box 是配置在堆積上且被回收記憶體的物件，所以過多 Boxing 和 unboxing 可能會對堆積、記憶體回收行程，以及最終是應用程式的效能造成負面影響。\n強制轉型為 Reference Type 時，Value Type 會進行 Boxed。 當強制轉型回實值型別時會進行 Unboxed，過多的 Boxed 和 Unboxed 會增加垃圾回收（GC）的負擔，從而影響應用程式效能。\n4. 複製參考 參考型別指派會複製參考，而實值型別指派則會複製整個值。 因此，大型參考型別的指派成本比大型實值型別的指派成本更低。\n5. 傳遞方式 參考型別會以傳址方式傳遞，而實值型別則是以傳值方式傳遞。對參考型別的執行個體所做的變更會影響所有指向執行個體的參考。實值型別執行個體會在以傳值方式傳遞時複製。變更實值型別的執行個體時，它必然不會影響其任何複本。\n由於不會由使用者明確建立複本，而是會在傳回引數或傳回值時隱含建立，因此可以變更的實值型別可能會對許多使用者造成混淆。 因此，實值型別應該為不可變。\n由於 Value Type 在傳遞過程中會複製，變更複本時不會影響原始值，這有時會讓使用者感到困惑，因為預期的變更並未反映在原始變數上。\n為了避免這種混淆，Value Type 通常應設計為不可變（immutable）。不可變的 Value Type 一旦創建，其狀態就無法改變，這樣可以確保每個複本都是一致且獨立的。\nReference Type 傳遞物件的記憶體地址。因此，對該物件所做的變更會影響所有持有該物件地址的參考。\n當 Reference Type 的物件進行變更時，因為所有指向該物件的參考都是同一個實例，所有持有該參考的變數都會受到變更影響。\nValue Type 會複製該值並將複本傳遞給目標位置。因此，對複本的變更不會影響原始值。\n當傳遞 Value Type 並對其進行變更時，因為是對複本進行操作，原始實例不會受到任何影響。這意味著每個複本都是獨立的。\n參考連結 一起學 Class and Struct\n","date":"2024-06-11T22:38:53+08:00","permalink":"/posts/c%23-value-typereference-type-%E7%9A%84%E5%B7%AE%E7%95%B0/","title":"C# Value Type、Reference Type 的差異"},{"content":"前言 這篇主要是在講虛擬代理人的實作部分。\n實作的主題的方向是讀取中央氣象署的氣象資訊。利用中央氣象署的 API 呼叫後，取得氣象資訊，並且在等待回應時實作虛擬代理設計。\nUML \u0026amp; Build WeatherProxy 使用 weather is null 判斷是否成功取得資料，並且 weatherDataTask == null || weatherDataTask.Status != TaskStatus.Running 來防止重複取得資料。\npublic string GetWeather(string area) { if (weather != null) { return weather.GetWeather(area); } else { if (weatherDataTask == null|| weatherDataTask.Status != TaskStatus.Running) { weatherDataTask = Task.Run(FetchWeatherData); } return \u0026#34;天氣資料更新中...\\n\u0026#34;; } } 完整程式碼\npublic class WeatherProxy : IWeather { /// \u0026lt;summary\u0026gt; /// https://opendata.cwa.gov.tw /// https://opendata.cwa.gov.tw/dist/opendata-swagger.html /// \u0026lt;/summary\u0026gt; private const string Url = \u0026#34;https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-D0047-073\u0026#34;; private const string AuthorizationKey = \u0026#34;AuthorizationKey\u0026#34;; private const string Format = \u0026#34;JSON\u0026#34;; private IWeather weather; private Task weatherDataTask; public string GetWeather(string area) { if (weather != null) { return weather.GetWeather(area); } else { if (weatherDataTask == null|| weatherDataTask.Status != TaskStatus.Running) { weatherDataTask = Task.Run(FetchWeatherData); } return \u0026#34;天氣資料更新中...\\n\u0026#34;; } } private async Task FetchWeatherData() { weather = new Weather(await SendRequest()); } private async Task\u0026lt;Root\u0026gt; SendRequest() { using var client = new HttpClient(); var query = HttpUtility.ParseQueryString(string.Empty); query[\u0026#34;Authorization\u0026#34;] = AuthorizationKey; query[\u0026#34;format\u0026#34;] = Format; var builder = new UriBuilder(Url); builder.Query = query.ToString(); string requestUri = builder.ToString(); string responseJson = await client.GetStringAsync(requestUri); return string.IsNullOrEmpty(responseJson) ? null : JsonSerializer.Deserialize\u0026lt;Root\u0026gt;(responseJson); } } GitHub ","date":"2024-04-30T00:39:46+08:00","permalink":"/posts/%E5%BF%83%E5%BE%97-%E8%99%9B%E6%93%AC%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F/","title":"心得 虛擬代理模式"},{"content":"前言 主要是用來解決大部與後端同步資料的方法，在實作方面代理人模式是最常見的解決方案。\n討論 Q1 代理人模式實作方向問題 多人連線後與後端資料同步\n不過確實在實作主題選擇有限，尤其遠端代理需要與後端同步資料，在目前公司遊戲的架構是不需要這樣做的。\nQ2 需要與後端同步記憶體資料嗎？ 不一定，Java RMI、C# WCF、Android AIDL 可以做到同步記憶體資料。\n","date":"2024-04-15T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E4%BB%A3%E7%90%86%E4%BA%BA%E6%A8%A1%E5%BC%8F/","title":"心得 代理人模式"},{"content":"前言 狀態模式主要用來解決多個 if else 判斷，並且不會因為多了一個 if，導致很多方法都要重新寫判斷。\n討論 我提出的問題是狀態模式是由狀態(State) 來控制前往哪個 State，為什麼不是狀態機(Context)來去控制 State 流程。\nState 控制 State 切換 優點：控制流程可以簡化 if else 的判斷，因為在 State 會少很多判斷。\n缺點：也因為在 State 裡面判斷，在 Context 是無法知道什麼時候切換 State。\nContext 控制 State 切換 優點：可以明確的知道切換時機，並且也知道切換的 State。\n缺點：在 Context 切換 State 會有較多的 if else 判斷。\n結論 結論策略模式(Strategy Pattern)與狀態模式的差異，有同事提出是 State 控制 State 切換。假如由 Context 控制 State 切換，就跟策略模式類似。\n我後來想想也有點道理，Context 控制 State 切換並沒有解決多個 if else 判斷，且與策略模式類似。\n","date":"2024-04-10T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E7%8B%80%E6%85%8B%E6%A8%A1%E5%BC%8F/","title":"心得 狀態模式"},{"content":"前言 組合模式是需要 Leaf（子節點）的嗎？\n這是我提出來的問題，因為我覺得 Composite，同時也繼承了 Component，這樣跟 Leaf 也有繼承 Component 差不多。\n後來我覺得也許拆開是為了職責問題，Composite、Leaf 實現功能可能不一樣。\n我後來也參考了別人的定義，其中 Composite、Leaf 的定義有明確職責。\nComponent: 是一個抽象類別，組合模式中的物件都繼承此類別。其中， inflate() 是每一個物件都要實作的功能。 add(Component) 以及 remove(Component) 都是提供給 Composite 類別組合 Leaf 使用的。 Leaf : 是一個具象類別 (Concrete class)，實作 Component 所定義的 inflate()方法，為一個最小單位的物件，在這裏不能包含其他 Leaf 。 Composite ：同樣也是一個具象類別 (Concrete class)，除了實作 Component 定義的 inflate() 方法。將多個 Leaf 紀錄在一個列表 components 裡利用 add(Component) 以及 remove(Component) 來對列表做處理。 結論 讀完這章節後，在我的個人是沒想到自己能使用的環境，可能使用環境是偏像資料搜尋、物件搜尋等。\n","date":"2024-04-01T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E7%B5%84%E5%90%88%E6%A8%A1%E5%BC%8F/","title":"心得 組合模式"},{"content":"IEnumerator、IEnumerable 使用 for、foreach 的差別 Q：實作發現 IEnumerable 能使用 foreach、不能使用 for，IEnumerator 能使用 for、不能使用 foreach，這是為什麼。\nA：IEnumerator 是無法使用 for，foreach 就是跑迭代器。需要有 index 的概念建議使用 for。\nAforeach 裡面的 List 陣列有改變的話，會出現錯誤。\nC# 迭代器延遲執行 A：使用 LinQ 時需要注意到延遲執行的部分。\n是否能夠使用迭代器製作 C# List A：可以，可是想自己寫 yield return 是無法的這是 C# 內建的。\nC# 底層都有繼承 IEnumerable 那是否不需要迭代器模式了？ A：實作不太可能，因為會為了做而做，除非是這個類別很特別才需要。\nUnity 使用 for、foreach 的效能，使用 foreach 會比較耗效能嗎？ A：這是 Unity 舊版 bug。一般用法還好除非是在 Update 使用 foreach 才會有明顯的差異。\nA：因為 Unity C# 與 MS C# 不太一樣，在使用 Unity 物件時不要使用 ?. 的方式。\nNull 條件運算子 ?. 和 ?[]\nA?.B(); 更理解迭代器運作方式 可以先理解 LinQ 運作方式，就可以知道內部迭代跟外部的差異。\n再來可以理解 C# IEnumerator、IEnumerable，要搭配 for、foreach，並且搭配 yield return，就可以理解 yield return 在迭代器的概念。\n最後實作 Unity 協成(Coroutine)，寫一個方法 to IEnumerator 並且使用 StarCoroutine，並且不使用 UnityEngine。\nIEnumerable 在方法上使用是什麼情況會使用到？ A：延遲執行，可以在需要使用的時候再呼叫就可。\nQ：假如不懂的話是不是會是一個坑？\nA：非同步執行或感受非同步執行時使用。希望執行這行程式的時候，不要卡死在這邊，延遲執行可以等資料匯入後再執行。不懂的人使用確實會是一個坑。\n","date":"2024-03-26T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F/","title":"心得 迭代器模式"},{"content":"前言 模板模式最基礎的概念就是需要一個骨架(abstract class)，利用不同方法或流程來實作，算是很常見的設計模式之一。\n模板模式的骨架可搭配泛型使用，可是不能說使用泛型就是模板模式，主要還是要符合骨架、有不同的方法或流程，這幾個要點來區分。\n掛勾 掛勾的部分，主要利用掛勾來實現方法職責，讓模板模式彈性更多一些。在模板模式中掛勾主要是用來判斷流程走向。\n模板模式 vs 策略模式 策略模式主要是針對行為來制定，而模板模式使用相同的骨架，實作不同的內容、流程。\n差異會在有沒有相同骨架、流程，在這部分則不是策略模式的重點。\n策略模式主軸是利用抽換不同的介面達到功能效果。\n好萊塢守則 vs 依賴反轉 好萊塢守則 只是思想，即「不要打電話給我們，我們會打給你」。相較於高階模組、低階模組，就沒有強制規定要不要符合，所以才是一種思想而不是一個設計概念。\n依賴反轉原則 高階模組不應該依賴於低階模組，兩者都應該依賴於抽象介面。\n抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面。\n依賴反轉原則確實符合好萊塢守則的概念。\n","date":"2024-03-18T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E6%A8%A1%E6%9D%BF%E6%A8%A1%E5%BC%8F/","title":"心得 模板模式"},{"content":"前言 當本身有所有使用到類別，就不必使用表象模式。也就是表象模式是需要有各種次系統組裝而成的模式，而不是覺得這邊很複雜就用表象模式。\n表象模式所有的次系統，必須要傳遞進來的物件，自己不能夠封裝類別。\n表象模式不建議使用抽象(interface、abstract)，除非真的知道功能需求。\n表象模式傳遞進來的次系統，不一定需要是介面，也可以傳遞實體物件。\n觀念 封裝成方法跟使用表象模式的差異 特性 表象模式 封裝成方法 目的 簡化介面 提高模組化和可重用性 應用範圍 複雜的系統 任何程式碼 實現方式 使用介面類別來封裝系統 使用方法來封裝邏輯 表象模式和封裝成方法都是有用的設計模式。表象模式可以用於簡化複雜系統的介面，而封裝成方法可以用於提高程式碼的模組化和可重用性。兩者可以結合使用以獲得更好的效果。\n迪米特守則 要注意迪米特守則，使用過度會讓 code 過多，理解難度增加，不必須要為了符合守則而寫出奇怪的設計。\n","date":"2024-03-12T00:00:00Z","permalink":"/posts/%E5%BF%83%E5%BE%97-%E8%A1%A8%E8%B1%A1%E6%A8%A1%E5%BC%8F/","title":"心得 表象模式"},{"content":"主要是用來打包程式輸出成 .msi 檔（Windows Installer）。\n建議先看完 「30 天 | C# WixToolset + WPF 帥到不行的安裝包 系列」，能夠把 80% 以上的問題解決。\n輸出 Wix 文件方法 先到 WiX Toolset Release 安裝需要的工具。\n需要開啟 CMD 且必須要用工作管理員權限打開，之後 cd 到 WiX Toolset 的安裝資料夾。\nC:\\Program Files (x86)\\WiX Toolset v3.11\\bin heat.exe 可以掃描目錄中的所有文件和子目錄，並生成 WiX 文件中所需的 Component、Directory、File 和其他元素的定義。\n使用下面的參數可以拿到 myKeyIn.wxs。需要把 [MyDemo] 替換成正確的路徑，可以參考我的 Product.wxs。\nheat.exe dir \u0026#34;[PUT_YOUR_PATH]\u0026#34; -dr INSTALLFOLDER -cg ProductComponents -gg -gl -sf -srd -var \u0026#34;[MyDemo]\u0026#34; -out \u0026#34;[PUT_YOUR_OUTPUT_PATH]\\myKeyIn.wxs\u0026#34; 參數名稱 解釋 [PUT_YOUR_PATH] 需要打包的路徑資料夾 [MyDemo] WiX 文件中要使用的變數 [PUT_YOUR_OUTPUT_PATH] WiX 文件的路徑 Wix Product.wxs Product.wxs\n桌面捷徑 需要注意一些細項設定，如 Guid、執行檔名稱、路徑等。\n\u0026lt;Directory Id=\u0026#34;DesktopFolder\u0026#34; Name=\u0026#34;Desktop\u0026#34; \u0026gt; \u0026lt;Component Id=\u0026#34;DesktopFolderShortcut\u0026#34; Guid=\u0026#34;{94D38478-869E-4CA8-BEA6-7905A7135DB8}\u0026#34;\u0026gt; \u0026lt;Shortcut Id=\u0026#34;DesktopShortcut\u0026#34; Directory=\u0026#34;DesktopFolder\u0026#34; Name=\u0026#34;Wix-Hello Unity\u0026#34; Target=\u0026#34;[INSTALLFOLDER]Wix-Hello Unity.exe\u0026#34; WorkingDirectory=\u0026#34;INSTALLFOLDER\u0026#34; Icon=\u0026#34;WixToolsetIcon\u0026#34;\u0026gt; \u0026lt;/Shortcut\u0026gt; \u0026lt;RegistryValue Root=\u0026#34;HKCU\u0026#34; Key=\u0026#34;Software\\WenRongStudio\\Wix-Hello Unity\u0026#34; Name=\u0026#34;installed\u0026#34; Type=\u0026#34;integer\u0026#34; Value=\u0026#34;1\u0026#34; KeyPath=\u0026#34;yes\u0026#34;/\u0026gt; \u0026lt;/Component\u0026gt; \u0026lt;/Directory\u0026gt; 資料夾權限 避免安裝在系統槽時，無法變動檔案，所以必須把資料夾權限打開。\n需要注意一些細項設定，如 Guid 等。\n\u0026lt;Component Id=\u0026#34;InstallationDirectory\u0026#34; Guid=\u0026#39;{0D92D4B6-CACE-4556-8CBD-C8C385B22D28}\u0026#39; \u0026gt; \u0026lt;CreateFolder Directory=\u0026#34;INSTALLFOLDER\u0026#34;\u0026gt; \u0026lt;Permission User=\u0026#34;SYSTEM\u0026#34; GenericAll=\u0026#34;yes\u0026#34;/\u0026gt; \u0026lt;Permission User=\u0026#34;EveryOne\u0026#34; GenericAll=\u0026#34;yes\u0026#34; GenericRead=\u0026#34;yes\u0026#34; Read=\u0026#34;yes\u0026#34; ReadAttributes=\u0026#34;yes\u0026#34; GenericExecute=\u0026#34;yes\u0026#34; TakeOwnership =\u0026#34;yes\u0026#34; GenericWrite =\u0026#34;yes\u0026#34; WriteAttributes=\u0026#34;yes\u0026#34; ReadPermission =\u0026#34;yes\u0026#34; ChangePermission=\u0026#34;yes\u0026#34; /\u0026gt; \u0026lt;Permission User=\u0026#34;Users\u0026#34; Domain=\u0026#34;[LOCAL_MACHINE_NAME]\u0026#34; GenericAll=\u0026#34;yes\u0026#34; GenericRead=\u0026#34;yes\u0026#34; Read=\u0026#34;yes\u0026#34; ReadAttributes=\u0026#34;yes\u0026#34; GenericExecute=\u0026#34;yes\u0026#34; TakeOwnership =\u0026#34;yes\u0026#34; GenericWrite =\u0026#34;yes\u0026#34; WriteAttributes=\u0026#34;yes\u0026#34; ReadPermission =\u0026#34;yes\u0026#34; ChangePermission=\u0026#34;yes\u0026#34;/\u0026gt; \u0026lt;/CreateFolder\u0026gt; \u0026lt;/Component\u0026gt; 建立 Windows Menu 資料 可以 Windows Menu 啟動檔案跟卸載執行檔。\n需要注意一些細項設定，如 Guid、執行檔名稱、路徑等。\n\u0026lt;Directory Id=\u0026#34;ProgramMenuFolder\u0026#34;\u0026gt; \u0026lt;Directory Id=\u0026#34;ApplicationProgramsFolder\u0026#34; Name=\u0026#34;Wix-Hello Unity\u0026#34;\u0026gt; \u0026lt;Component Id=\u0026#34;ApplicationShortcut\u0026#34; Guid=\u0026#34;{9724BE7E-7132-4978-B806-4B0D1B3DE350}\u0026#34;\u0026gt; \u0026lt;Shortcut Id=\u0026#34;ApplicationStartMenuShortcut\u0026#34; Name=\u0026#34;Wix-Hello Unity\u0026#34; Description=\u0026#34;Wix-Hello Unity\u0026#34; Target=\u0026#34;[INSTALLFOLDER]\\Wix-Hello Unity.exe\u0026#34; WorkingDirectory=\u0026#34;APPLICATIONROOTDIRECTORY\u0026#34; Icon=\u0026#34;WixToolsetIcon\u0026#34;/\u0026gt; \u0026lt;Shortcut Id=\u0026#34;UninstallProduct\u0026#34; Name=\u0026#34;Uninstall\u0026#34; Description=\u0026#34;Uninstalls Wix-Hello Unity\u0026#34; Target=\u0026#34;[System64Folder]msiexec.exe\u0026#34; Arguments=\u0026#34;/x [ProductCode]\u0026#34; /\u0026gt; \u0026lt;RemoveFolder Id=\u0026#34;ProgramMenuSubfolder\u0026#34; On=\u0026#34;uninstall\u0026#34;/\u0026gt; \u0026lt;RemoveFolder Id=\u0026#34;ApplicationProgramsFolder\u0026#34; On=\u0026#34;uninstall\u0026#34;/\u0026gt; \u0026lt;RegistryValue Root=\u0026#34;HKCU\u0026#34; Key=\u0026#34;Software\\WenRongStudio\\Wix-Hello Unity\u0026#34; Name=\u0026#34;installed\u0026#34; Type=\u0026#34;integer\u0026#34; Value=\u0026#34;1\u0026#34; KeyPath=\u0026#34;yes\u0026#34;/\u0026gt; \u0026lt;/Component\u0026gt; \u0026lt;/Directory\u0026gt; \u0026lt;/Directory\u0026gt; GitHub 參考文章 用 WiX 制作安装包：创建一个简单的 msi 安装包\n30 天 | C# WixToolset + WPF 帥到不行的安裝包 系列\nCreate an Uninstall Shortcut\nCreate a Shortcut on the Start Menu\nRemoving files when uninstalling WiX\n","date":"2023-10-20T00:00:00Z","permalink":"/posts/wix-examples/","title":"Wix Examples"},{"content":"前言 在 Windwos 環境使用 Unity 輸出 XCode，之後使用 Mac 測試、上傳，出現了錯誤。\nCommand PhaseScriptExecution failed with a nonzero exit code 解決方式 因為專案有使用 Cardboard，且 Unity 是使用 2022，才會出現此問題，之前使用 2021 輸出上架都沒問題。\n我的解決方法是把專案改成在 MacOS 上輸出就能完美解決此問題。\n測試過的方法 有測試過的方法，可是對我這次情況沒有效果。\n升級或安裝 Pod。參考 修改 build phases 開啟 For install builds only。參考 修改 Workspace Setting 的 Build System，在 Xcode 14 無法修改。參考 ","date":"2023-03-08T00:00:00Z","permalink":"/posts/xcode-command-phasescriptexecution-failed/","title":"Xcode Command PhaseScriptExecution Failed"},{"content":"前言 在 Android 11 以上的版本使用 VideoPlayer 呼叫 Stop(); 時會造成 App Crash。\n官方論壇討論此問題文章。\n錯誤 log Stack trace: Error AndroidRuntime signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 Error AndroidRuntime Cause: null pointer dereference Error AndroidRuntime r0 00000000 r1 00003f06 r2 71303e68 r3 00000002 Error AndroidRuntime r4 0848fed2 r5 e5f9b138 r6 ea0e93e0 r7 00000000 Error AndroidRuntime r8 b5c5bfa8 r9 00000000 r10 b5c5bfe8 r11 00000002 Error AndroidRuntime ip e9e63e58 sp b5c5bf00 lr e9df8263 pc e9d823fa Error AndroidRuntime Error AndroidRuntime backtrace: Error AndroidRuntime #00 pc 000773fa /system/lib/libandroid_runtime.so (BuildId: cb59abe29c72e464af331ce6551ec035) Error AndroidRuntime #01 pc 00000136 [anon:.bss] Error AndroidRuntime Error AndroidRuntime at libandroid_runtime.0x773fa(Native Method) Error AndroidRuntime at [anon:.0x136(Native Method) 解決方式 官方在論壇回覆是建議回去 2020 版，之後會修復。因為我使用的專案不方便降版。\n我解決的方式 VideoPlayer.Pause()，然後生成一個新的 VideoPlayer 物件，原本舊的 VideoPlayer 物件不要關閉物件、不要刪除物件，不然都會造成 App Crash。\n不過最終解決方式還是需要等官方處理結束，可以看這個 bug 什麼時候解決 Issue Tracker。\n","date":"2023-02-01T00:00:00Z","permalink":"/posts/unity2021-video-player-crash/","title":"Unity2021 Video Player Crash"},{"content":"前言 此篇是優化原本的 Unity ParticlePath\n簡介 有使用 Job System 優化功能。\n100000 顆粒子使用路徑功能時，SAMSUNG GALAXY S7 在不使用 Job System FPS 約 8-9 FPS，使用後變成 18-20 FPS，PC 版多使用了 Burst 會從 30 FPS 提升至 100 FPS。\n可能因為測試的硬體裝置數據優化有所不同，建議還是實際測試後才決定。\n使用方式 可以先使用 demo 場景測試，必須要打開 IsJob，才會啟動 Job System。\nGitHub ","date":"2023-01-18T00:00:00Z","permalink":"/posts/unity-particlebezierpath/","title":"Unity ParticleBezierPath"},{"content":"前言 這是簡易的 Oculus 功能介紹專案，是我製作 VR 專案以來經常使用到的操作行為，我把這些操作行為歸類起來。\n功能接紹有按鈕、抓取、放置、按壓、等操作行為，目前只要有這些功能就能完整製作 VR 的操作行為。\n配置 需要參考官方文件 Get Started with Oculus in Unity，也可以觀看該專案配置。\n","date":"2023-01-02T00:00:00Z","permalink":"/posts/oculus-sample/","title":"Oculus Sample"},{"content":"前言 deep link 可以用網址來當 link id，類似像手機點開 Youtube 網址時，假如裝置內有 Youtube App 就會自動開啟 App，並且切換至該影片內容。而 deep link 也可以用網址來當 link id 達到這樣的效果。\n也可以用來呼叫 app 時，假如該裝置沒有安裝可以直接轉移到 app store 上面讓使用者直接下載該 app。\nAndroid 需要在 AndroidManifest 上寫上 link id，可以根據 Create Deep Links to App Content 參考詳細的設置方式。\nurl 呼叫方式 \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.VIEW\u0026#34; /\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34; /\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.BROWSABLE\u0026#34; /\u0026gt; \u0026lt;data android:scheme=\u0026#34;http\u0026#34; /\u0026gt; \u0026lt;data android:scheme=\u0026#34;https\u0026#34; /\u0026gt; \u0026lt;data android:host=\u0026#34;wenrongdev.com\u0026#34; /\u0026gt; \u0026lt;data android:pathPrefix=\u0026#34;/unitydeeplink_2\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; host：網址\npathPrefix：節點\n可以利用這種方式，app 超連結開啟或者網頁輸入 https://wenrongdev.com/posts/unitydeeplink_2/ 時就會自動對應到 App。\n自訂 id \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.VIEW\u0026#34; /\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34; /\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.BROWSABLE\u0026#34; /\u0026gt; \u0026lt;data android:scheme=\u0026#34;app\u0026#34; /\u0026gt; \u0026lt;data android:host=\u0026#34;wenrongdev\u0026#34; /\u0026gt; \u0026lt;/intent-filter\u0026gt; 可以利用這種方式，app 超連結開啟或者網頁輸入 app://wenrongdev 時就會自動對應到 App。\niOS Unity 設定 link id 的方式在 Deep linking on iOS 有介紹如何設定，而 Xcode 也能設定，不過建議是在 Unity 中設定非必要不建議額外自己在手動設置，主要是怕輸出時忘記導致功能失效。\nXcode 詳細設定可以參考這邊 IOS Deep linking: URL Scheme vs Universal Links\n這樣設定後就會與 Android 一樣的功能，在瀏覽器輸出該 link id 就會自動對應到 app。\n其他 Github Unity Deep Link -1 ","date":"2022-12-13T00:00:00Z","permalink":"/posts/unity-deep-link-2/","title":"Unity Deep Link -2"},{"content":"前言 DeepLink 是可以直接用網址呼叫 App 的方式之一，以前有提到可以利用 Get Android Intent Data for Unity 這邊文章提到的方式，呼叫 App，不過這只限定 Android，iOS 則還是需要用 Deeplink 的方式呼叫。主要是當時 Unity 版本並不支援 DeepLink，所以只能自己寫原生的，才會有之前的這篇文章，更重要的是，使用之前的呼叫方式是需要某些權限，但目前 Google 也把這些權限關閉，無法正常上架需要自己寫信去解釋才會願意讓你正常上架。所以建議是棄用這種方法改用 Deeplink。\n詳細資料 Unity Deep Link\nAndroid Deep Link\niOS Deep Link\n建議是一定要把 Unity Deep Link 看完，才會知道怎麼設定，其餘兩邊則是原生地設定方式，可以參考。\n使用方式 Unity Deep link Doc\n官方也有文件解釋 Deep link 的基礎設定。\nScript Start 需要再被喚醒 app 的瞬間也就是 Awake 時，先讀取 Application.absoluteURL 才能讀取道 deep link 的資料。Application.deepLinkActivated 部分則是 app 的 deep link feedback。\nprivate void Awake() { Application.deepLinkActivated += OnDeepLinkActivated; if (!string.IsNullOrEmpty(Application.absoluteURL)) OnDeepLinkActivated(Application.absoluteURL); } Script Url Arg 拆解 Deep link 夾帶的參數，格式大概與 web 的 url get 類似，可以用這種方式去解析，夾帶的參數。\nprivate void OnDeepLinkActivated(string url) { string[] urlArg = url.Split(\u0026#39;?\u0026#39;); string[] args = new string[0]; if (urlArg.Length \u0026gt; 1) { char[] charSeparators = new char[] { \u0026#39;\u0026amp;\u0026#39; }; args = urlArg[1].Split(charSeparators, StringSplitOptions.RemoveEmptyEntries); } for (int i = 0; i \u0026lt; args.Length; i++) { Debug.Log(args[i]); } } 其他 Github Unity Deep Link -2 ","date":"2022-12-07T00:00:00Z","permalink":"/posts/unity-deep-link-1/","title":"Unity Deep Link -1"},{"content":"前言 Unity Text 中英混雜導致英文字跳行問題，主要是 Space 字串的問題，可以用 \\u00A0 替代 Space。\nCode private static readonly string no_breaking_space = \u0026#34;\\u00A0\u0026#34;; public static string ReplaceSpace(string context) { return context.Replace(\u0026#34; \u0026#34;, no_breaking_space); } 可以這樣替代全部的 Space。\n範例 使用前 使用後 ","date":"2022-12-03T00:00:00Z","permalink":"/posts/unity-text-breaking-space/","title":"Unity Text Breaking Space"},{"content":"前言 之前因為遇到多平台功能，要輸出時各個平台 Player Setting 細項設定皆為不同，會因為某些沒有設定導致輸出時出包，所以才寫了一個自動輸出個平台功能。\nXR Setting 可以利用這段來新增或移除 XR 裡面的 Oculus 勾選。\nprivate static void SetOculusXRLoader(BuildTargetGroup buildTarget, bool active) { XRGeneralSettingsPerBuildTarget buildTargetSettings = null; EditorBuildSettings.TryGetConfigObject(XRGeneralSettings.k_SettingsKey, out buildTargetSettings); XRGeneralSettings settings = buildTargetSettings.SettingsForBuildTarget(buildTarget); if (active) XRPackageMetadataStore.AssignLoader(settings.Manager, \u0026#34;Unity.XR.Oculus.OculusLoader\u0026#34;, buildTarget); else XRPackageMetadataStore.RemoveLoader(settings.Manager, \u0026#34;Unity.XR.Oculus.OculusLoader\u0026#34;, buildTarget); } Build 利用這段自動輸出，options 可以設定 BuildOptions.None、BuildOptions.AutoRunPlayer，一般的 Build 和 Build and Run。\nprivate static void BuildRelease(string Path, BuildTarget Target, BuildOptions options) { Console.Clear(); BuildPlayerOptions playerOptions = GetBuildPlayer(Path, Target, options); BuildReport Report = BuildPipeline.BuildPlayer(playerOptions); EditorUtility.RevealInFinder(Path); Debug.Log(string.Format(\u0026#34;{0} Build completed with a result of \u0026#39;{1}\u0026#39; \u0026#34;, Application.platform, Report.summary.result.ToString())); } private static BuildPlayerOptions GetBuildPlayer(string path, BuildTarget Target, BuildOptions options) { return new BuildPlayerOptions() { scenes = EnabledScenePaths, locationPathName = path, target = Target, options = options }; } 完整 Script public class SampleBuildRelease { private static string AppName =\u0026gt; PlayerSettings.productName; private static string Version =\u0026gt; Application.version.Replace(\u0026#34;.\u0026#34;, \u0026#34;\u0026#34;); private static string BuildFolder { get { return Directory.GetParent(Application.dataPath).FullName.Replace(\u0026#39;\\\\\u0026#39;, \u0026#39;/\u0026#39;) + \u0026#34;/Build\u0026#34;; } } private static string[] EnabledScenePaths =\u0026gt; EditorBuildSettings.scenes .Where((scene) =\u0026gt; scene.enabled) .Select((scene) =\u0026gt; scene.path) .ToArray(); [MenuItem(\u0026#34;Builds/Build/Oculus\u0026#34;)] public static void Build_Oculus() { string path = Path.Combine(BuildFolder, \u0026#34;Oculus\u0026#34;, $\u0026#34;Oculus_{AppName}_Ver{Version}.apk\u0026#34;); BuildReleaseOculus(path, BuildOptions.None, true); } [MenuItem(\u0026#34;Builds/Build And Run/Oculus\u0026#34;)] public static void BuildAndRun_Oculus() { string path = Path.Combine(BuildFolder, \u0026#34;Oculus\u0026#34;, $\u0026#34;Oculus_{AppName}_Ver{Version}.apk\u0026#34;); BuildReleaseOculus(path, BuildOptions.AutoRunPlayer, true); } private static void BuildReleaseOculus(string path, BuildOptions buildOptions, bool AddXR) { SetOculusXRLoader(BuildTargetGroup.Android, AddXR); BuildRelease(path, BuildTarget.Android, buildOptions); } [MenuItem(\u0026#34;Builds/Build/Windows\u0026#34;)] public static void Build_Windows() { BuildReleaseWindows(BuildOptions.None); } [MenuItem(\u0026#34;Builds/Build And Run/Windows\u0026#34;)] public static void BuildAndRun_Windows() { BuildReleaseWindows(BuildOptions.AutoRunPlayer); } private static void BuildReleaseWindows(BuildOptions buildOptions) { string folder = Path.Combine(BuildFolder, $\u0026#34;{BuildTarget.StandaloneWindows}\u0026#34;, $\u0026#34;Windows_{AppName}_Ver{Version}\u0026#34;); string path = Path.Combine(folder, $\u0026#34;{AppName}.exe\u0026#34;); SetOculusXRLoader(BuildTargetGroup.Standalone, false); BuildRelease(path, BuildTarget.StandaloneWindows, buildOptions); } private static void SetOculusXRLoader(BuildTargetGroup buildTarget, bool active) { XRGeneralSettingsPerBuildTarget buildTargetSettings = null; EditorBuildSettings.TryGetConfigObject(XRGeneralSettings.k_SettingsKey, out buildTargetSettings); XRGeneralSettings settings = buildTargetSettings.SettingsForBuildTarget(buildTarget); if (active) XRPackageMetadataStore.AssignLoader(settings.Manager, \u0026#34;Unity.XR.Oculus.OculusLoader\u0026#34;, buildTarget); else XRPackageMetadataStore.RemoveLoader(settings.Manager, \u0026#34;Unity.XR.Oculus.OculusLoader\u0026#34;, buildTarget); } private static void BuildRelease(string Path, BuildTarget Target, BuildOptions options) { Console.Clear(); BuildPlayerOptions playerOptions = GetBuildPlayer(Path, Target, options); BuildReport Report = BuildPipeline.BuildPlayer(playerOptions); EditorUtility.RevealInFinder(Path); Debug.Log(string.Format(\u0026#34;{0} Build completed with a result of \u0026#39;{1}\u0026#39; \u0026#34;, Application.platform, Report.summary.result.ToString())); } private static BuildPlayerOptions GetBuildPlayer(string path, BuildTarget Target, BuildOptions options) { return new BuildPlayerOptions() { scenes = EnabledScenePaths, locationPathName = path, target = Target, options = options }; } } ","date":"2022-12-01T00:00:00Z","permalink":"/posts/oculus-auto-set-build-setting-build/","title":"Oculus Auto Set Build Setting \u0026\u0026 Build"},{"content":"前言 針對 Unity Shader 檔案打開時，使用 VSCode 開啟而不是 Visual Studio，假如預設是 VSCode 則無需使用這功能。\n會寫這功能是平常寫 C# 都是習慣使用 Visual Studio，而 Visual Studio 好像沒有針對 Unity Shaderlab 的關鍵字，而 VSCode 則有 ShaderlabVSCode(Free)，也因此這樣選擇使用 VSCode。\n因為平常開發時都是使用 Visual Studio，想開啟 Shader 時可以直接使用 VSCode 編輯，因此才參考 Sublime Text \u0026amp; Unity Shader，把 Sublime Text 改成使用 VSCode。\n環境變數 需要注意環境變數裡的使用者變數的 Path 需要有 VSCode 的路徑\n也可以使用 CMD 測試有無環境變數\nVSCode CLI Args 根據 Unity 開啟 VSCode Args，可以使用 Process 填寫對應路徑就可以了。\n\u0026#34;$(ProjectPath)\u0026#34; -g \u0026#34;$(File)\u0026#34;:$(Line):$(Column) startInfo.Arguments = $\u0026#34;{projectPath} -g {fileName}\u0026#34;; Script 詳細的方法可以參考 Sublime Text \u0026amp; Unity Shader。\npublic class OpenShaderForVSCodeEditor { [UnityEditor.Callbacks.OnOpenAsset(0)] public static bool CallbackShader(int instanceID, int line) { string projectPath = Directory.GetParent(Application.dataPath).ToString(); string strFilePath = AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID)); string fileName = projectPath + \u0026#34;/\u0026#34; + strFilePath; if (fileName.EndsWith(\u0026#34;.shader\u0026#34;)) { var envUser = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User); var envPaths = envUser[\u0026#34;Path\u0026#34;].ToString().Split(\u0026#34;;\u0026#34;); string vscodePath = string.Empty; for (int i = 0; i \u0026lt; envPaths.Length; i++) { var path = Path.Combine(envPaths[i], \u0026#34;code\u0026#34;); if (File.Exists(path)) { vscodePath = path; break; } } if (!string.IsNullOrEmpty(vscodePath)) { Process process = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.FileName = vscodePath; ///vscode args \u0026#34;$(ProjectPath)\u0026#34; -g \u0026#34;$(File)\u0026#34;:$(Line):$(Column) startInfo.Arguments = $\u0026#34;{projectPath} -g {fileName}\u0026#34;; process.StartInfo = startInfo; process.Start(); return true; } else { UnityEngine.Debug.Log(\u0026#34;Not Found Enviroment Variable \u0026#39;VSCode_Path\u0026#39;.\u0026#34;); return false; } } return false; } } GitHub ","date":"2022-08-09T00:00:00Z","permalink":"/posts/unity-open-shader-for-vscode/","title":"Unity Open Shader For VSCode"},{"content":"前言 介紹我使用 Addressable 的讀取、生成、釋放方式。\n詳細設定還是可以先觀看官方文件。\nEvent Viewer 可以利用 Event Viewer 在 Editor Runtime 時，隨時監控記憶體使用情況。就可以很明顯的發現，那些東西是忘記釋放掉的，或者不需要釋放的。\nLoad Asset IEnumerator InstantiateAsset(string asset) { AsyncOperationHandle\u0026lt;GameObject\u0026gt; async = asset.LoadAssetAsync\u0026lt;GameObject\u0026gt;(); yield return async; GameObject go = Instantiate(async.Result); } IEnumerator InstantiateAssets(string label) { AsyncOperationHandle\u0026lt;IList\u0026lt;GameObject\u0026gt;\u0026gt; async = Addressables.LoadAssetsAsync\u0026lt;GameObject\u0026gt;(label, null); yield return async; for (int i = 0; i \u0026lt; async.Result.Count; i++) GameObject go = Instantiate(async.Result[i]); } IEnumerator InstantiateAsset(AssetReference asset) { AsyncOperationHandle\u0026lt;GameObject\u0026gt; async = asset.LoadAssetAsync\u0026lt;GameObject\u0026gt;(); yield return async; GameObject go = Instantiate(async.Result); } 可以利用這三種方式讀取 Asset，並且把物件生成出來，當然我還是最推薦使用 AssetReference，除非有什麼特殊需求要使用字串，不然我不會換成其他方式。\nRelease Asset 釋放掉 Asset 也記得要把物件刪除，不然場上會遺留破圖的物件。\nprivate void ReleaseAsset(AsyncOperationHandle\u0026lt;T\u0026gt; async) { Addressables.Release(async); } private void ReleaseAsset(T asset) { Addressables.Release(asset); } private void ReleaseAsset(AssetReference asset) { asset.ReleaseAsset(); } 可以利用生成的物件或者 Load asset 的 Async 釋放，假如是使用 AssetReference Load Asset，也可以使用這個方式釋放。\n","date":"2022-05-04T00:00:00Z","permalink":"/posts/unity-addressable-load-assets/","title":"Unity Addressable Load Assets"},{"content":"前言 在前一篇 Unity Addressable 介紹了簡易使用的方式，此篇是介紹下載 Asset 方法。\n目前我個人使用過的方式有 Name、Label、AssetReference、Array AssetReference 的方式。\nAddressables.LoadAssetAsync 官方範例提供的下載方式 Addressables.LoadAsset(s)Async，雖然這個是要把 Asset 讀取出來，其實他也有下載功能\nAddressables.LoadAssetAsync\u0026lt;GameObject\u0026gt;(asset); 我是很少使用這種方式下載或更新 Asset，主因是我個人認為 Addressables.LoadAssetAsync 是讀取物件而不是更新物件的功能，所以我在需要更新時不會使用它。\nUpdate Addressable Name/ Addressable Label IEnumerator UpdateAsset(string asset) { var downloadAsync = Addressables.DownloadDependenciesAsync(asset, false); yield return downloadAsync; Addressables.Release(downloadAsync); } 這個是下載 Addressable Name 或 Label的方法，可以利用 Label 下載多個不同 Group 的 Asset。\nAddressable Name、Label 是無法利用 Hotfix 的方式產生，所以是要先創好需要的。若假如需要新的 Addressable Name、Label 是必須要重新輸出 App 才會更新。\nUpdate Asset Reference IEnumerator UpdateAsset(AssetReference asset) { var downloadAsync = Addressables.DownloadDependenciesAsync(asset, false); yield return downloadAsync; Addressables.Release(downloadAsync); } AssetReference 的用法是我最推薦的下載方式，可以很明顯的知道更新物件，不會因為打錯文字導致更新失敗，而且也可以利用這個方式組合物件，讓更新的內容比較簡單方式處理。\n例如用一個物件或場景夾帶了多個不同的需要更新的物件，缺點就是這包 pack 輸出會過大，可能需要把每個物件獨立分成多個 AssetReference，利用系統特性夾帶物件變小。不過要是不喜歡這種方式可以看 Update Multiple Asset References。\n不過 AssetReference 最方便還是使用它來生成、釋放、等等，才是最好用的方式。\nUpdate Multiple Asset References IEnumerator DonwloadMultipleAssets(AssetReference[] assets) { var assetKeys = assets.Cast\u0026lt;AssetReference\u0026gt;(); var downloadAsync = Addressables.DownloadDependenciesAsync(assetKeys, Addressables.MergeMode.Union); yield return downloadAsync; Addressables.Release(downloadAsync); } 多個 AssetReference 更新的方式，然後可以一起更新。可以完成一個簡單的多個物件更新，不用利用 Label、整合包、等方式更新。\n取得下載容量大小方法 IEnumerator CheckSizeAsync(string asset) { var async = Addressables.GetDownloadSizeAsync(asset); yield return async; if (async.Status == AsyncOperationStatus.Succeeded) { float size = Mathf.Round((float)async.Result / 1024 / 1024 * 1000) / 1000; Debug.Log($\u0026#34;Total {size} MB\u0026#34;); } Addressables.Release(async); } 更新進度條寫法 IEnumerator UpdateAsset(AssetReference asset) { var downloadAsync = Addressables.DownloadDependenciesAsync(asset, false); while (!downloadAsync.IsDone) { float percent = downloadAsync.PercentComplete; Debug.Log($\u0026#34;{asset}: {downloadAsync.PercentComplete * 100} %\u0026#34;); yield return new WaitForEndOfFrame(); } Addressables.Release(downloadAsync); } ","date":"2022-03-14T00:00:00Z","permalink":"/posts/unity-addressable-download-assets/","title":"Unity Addressable Download Assets"},{"content":"前言 輸出 Apk 遇到的錯誤\nFix Installed Build Tools revision 3X.0.0 is corrupted. 修改方式 修改 d8.bat 檔案路徑 \u0026lt;Android SDK root\u0026gt;\\build-tools\\3X.0.0\n將 d8.bat 改為 dx.bat。\n修改 d8.jar 檔案路徑 \u0026lt;Android SDK root\u0026gt;\\build-tools\\3X.0.0\\lib\n將 d8.jar 改為 dx.jar。\n就完成修改。\n參考連結 Android Studio error \u0026ldquo;Installed Build Tools revision 31.0.0 is corrupted\u0026rdquo;\n","date":"2022-03-07T00:00:00Z","permalink":"/posts/android-build-failed-build-tools-3x.0.0-bug/","title":"Android Build Failed Build tools 3X.0.0 Bug"},{"content":"前言 此篇在講如何使用 Hugo 在 Github 上自架 Blog。\n安裝 Hugo 關於安裝的部分可以參考這些文章\nQuick Start GitHub 部署 Hugo 靜態網站 使用 Hugo 建立靜態網站，並部署在 Github Page Hugo 貼身打造個人部落格 系列 與文章不同的地方主題我是選擇 PaperMod，由於需要設定 config.yml，建議先參考 PaperMod-Installation。\n想要完整的設定或理解 PaperMod，最好是完整的看完 PaperMod 的 repo，有不知道的問題可以在 Issues 或者 FAQs 搜尋看看，會比 google 搜尋來得快。\nPaperMod 由於 PaperMod 推薦使用 config.yml，因此推薦把原本的 config.toml 刪除。並且去複製官方提供的 config.yml。\nconfig.yml Search Post 先在 config menu main 新增一個 Search 頁面\nmenu: main: - identifier: archives name: Archives url: /archives/ weight: 5 - identifier: tags name: Tags url: /tags/ weight: 10 - identifier: search name: Search url: /search/ weight: 20 且在最底下此內容。（參考文件）\noutputs: home: - HTML - RSS - JSON # is necessary 最後在專案的 content 底下新增 search.md，即可完成功能。（參考文件）\n--- title: \u0026#34;Search\u0026#34; # in any language you want layout: \u0026#34;search\u0026#34; # is necessary # url: \u0026#34;/archive\u0026#34; # description: \u0026#34;Description for Search\u0026#34; summary: \u0026#34;search\u0026#34; placeholder: \u0026#34;placeholder text in search input box\u0026#34; --- Comments 此功能是參考 Day 20. Hugo Comments System 文章製作出來的。\nGithub Action 有使用 Custom domain 的話，且 workflows 沒有設定 domain 的話，會造成每次更新文章時，都會清掉 Custom domain，變回原本的 github.io。\nGitHub Pages workflow.yml name: GitHub Pages on: push: branches: - main # Set a branch to deploy pull_request: jobs: deploy: runs-on: ubuntu-20.04 concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v2 with: submodules: true # Fetch Hugo themes (true OR recursive) fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: \u0026#34;0.91.2\u0026#34; # extended: true - name: Build run: hugo --minify - name: Deploy uses: peaceiris/actions-gh-pages@v3 if: ${{ github.ref == \u0026#39;refs/heads/main\u0026#39; }} with: github_token: ${{ secrets.HUGO_DEPLOY_TOKEN }} PUBLISH_BRANCH: gh-pages # 推送到 gh-pages 分支 commit_message: ${{ github.event.head_commit.message }} publish_dir: ./public cname: wenrongdev.com 只需要新增或替換掉 cname 後面為 domain 即可。\n結論 Hugo 或 PaperMod 我都處於摸索階段。\n這次架起來的感覺各方面都不錯，不管是讀取 Blog 速度、支援 Markdown、架設 Github 上且保留原始檔，到目前為止沒有明顯的缺點。\n目前最大問題就是 SEO，這是我完全沒有接觸過的。問題在於 Google 搜尋不到我的 Blog，所以這是之後要研究的部分。\n","date":"2022-02-28T00:00:00Z","permalink":"/posts/blog-%E5%BF%83%E5%BE%973/","title":"Blog 心得（3）"},{"content":"前言 由於 SiteGround 費用問題，因此我需要把文章轉移至別的地方。不過也知道自己需要哪些功能。\n免費 Server 網頁速度快 支援程式碼片段 自訂網域 這些是我目前需要的功能，不需要排版、SEO、等功能，最好是直接把 Markdown 語法貼上就能變成文章，剩下就都可以接受了。\nMedium 先說結論 Medium 不適合需要貼上 Code 的技術類文章。\n貼程式碼片段太麻煩。\n使用 Markdown 語法撰寫文章，因為格式不同需要重新排版。\n以上這兩點是使用後遇到的難題，導致我不得不再尋找新的平台。也是這時候才知道自己的 Blog 需要什麼樣的功能。最後在 寫技術部落格不需要那麼大費周章 發現了 CoderBridge，並且申請了 Blog。\nCoderBridge 完整提供 前言 提到所有功能，不過我最後還是選擇使用 Hugo 自架 Blog。\n主因是文章沒有備份檔。\n因為我自己的壞習慣，所以才都沒有留住這些檔案。也因為從 Wordpress、Medium、CoderBridge 換了三個 Blog，覺得搬遷好麻煩，都沒有完整的文章檔案，每次都需要重新排版，才會想架一個免費而且可以保留文章檔案的 Blog。\n最後自己是選擇了使用 Hugo 框架架在 Github page 上。\n結論 Wordpress、Medium、CoderBridge 我覺得各有優缺點，假如自己從一開始就決定要找免費的，我會選擇使用 CoderBridge，不過也因為搬來搬去才知道自己需要哪些功能，最後使用 Hugo 也只是因為我自己覺得很潮而已。\n","date":"2022-02-21T00:00:00Z","permalink":"/posts/blog-%E5%BF%83%E5%BE%972/","title":"Blog 心得（2）"},{"content":"其實我的 Blog 是寫給自己看的。因為我時常忘記怎麼解決問題、怎麼製作功能、忘記是做了什麼蠢事。因此產生這個 Blog 用來記錄我的跌跌撞撞的過程。\n前言 在大學寫程式時遇到問題去 Google 查，很多時候很難下關鍵字，主要是自己犯蠢導致很難知道原因。自然而然也很難對症下藥，很多時候只是重打或參考(?)同學就能解決學生時期遇到的問題。\n當開始工作時的第一年內，遇到的問題基本上還是類似的，因為我實在是太菜了，會遇到一些其實很基本又很無言的低級錯誤，一直重複的犯錯麻煩其他同事幫忙 debug，所以我一直有一個念頭想架設自己的 Blog。記錄解決過程和解決方式，提醒未來自己有錯誤可以來這邊看，假如又可以幫助別人其實也不錯。\nBlogger 第一個 Blog 是使用大學某堂課程用到的 Blogger，其實也沒太認真研究這個 Blog，因為覺得不夠好看用了一下沒有認真的使用。\n多年之後看到 搞笑談軟工 的 Blog，才知道 Blog 的重要的部分其實不是選用什麼架構，持續寫作 才是最重要的核心。\nWordpress 2018 時覺得自己的學習成長曲線變慢了，可能是因為都接觸類似的東西，工作上也大部分內容都能應付，也不會像以前一樣回家繼續寫程式，簡單來講就是發現自己進入了舒適圈(?)。就很坎坷不安，於是就開始買書學習、跟人學習。\n2019 的某天 Unity 程式社群，有人發問推薦的程式書籍，而 阿祥的開發日常 的 Ted 回答了 軟技能代碼之外的生存指南。整本書內容都沒在講程式的部分，大部分內容都是在講工程師除了工作以外的事情，例如運動、投資、面試、等等，推薦沒看過的工程師可以買，對於我個人是獲益良多，也是因為看了才有想認真的自己架設 Blog。\n書的內容其實就有推薦架設屬於自己的 Blog，當時看到就想到我當年的想法，是沒有想要跟作者一樣利用 Blog 賺錢，畢竟技術不到家。倒是覺得可以當一個不錯的作品集，於是就開始在架設自己的 Wordpress。\n當時是看著 教學 文章邊看邊選擇，過程除了刷卡付錢沒有什麼難度，我在 NameCheap 買網址，虛擬主機則是使用 SiteGround。\n使用三年心得 (2019~2020) 年分 SiteGround NameCheap 花費(NTD) 第一年 1493 285 1778 第二年 5375 315 5690 第三年 0 368 368 總花費 7836 第一年 1778 元。而且很多功能可以玩、可以使用，整體使用是很滿意的。而到了第二年，沒有認真確認第一年花費多少就直接續約了，收到扣款通知才知道收費真的很貴。\n第一年跟第二年價格差了 3912 元，當時也沒有想到要退款，想說明年扣款時再來處理。\n到了第三年，就打算不續約 SiteGround，所以開始把文章轉移去 Medium，不過 NameCheap 是有持續付費的，畢竟費用還是較少，而且網址現在還在使用。\n後來用 Hugo 架設這個 Blog，才知道 Wordpress 的好，很多事情都很簡單就完成（SEO、theme、等等）。\n結論 整體而言 Wordpress 對於新手的我架設 Blog，確實是不錯的體驗，只要有錢基本都能處理。不過續約的花費太貴，所以才放棄使用 SiteGround，尋找一些免費的方式。\n當時使用 NameCheap 時也有被盜刷過，記得要綁定 2FA、密碼不要重複。 ","date":"2022-02-14T00:00:00Z","permalink":"/posts/blog-%E5%BF%83%E5%BE%971/","title":"Blog 心得（1）"},{"content":"前言 此專案為 Unity math quiz，是基於 Tutorial 2: Create a timed math quiz 文件改寫出來的專案。\nGithub ","date":"2021-11-29T00:00:00Z","permalink":"/posts/unity-mathquiz/","title":"Unity MathQuiz"},{"content":"前言 使用方式 預設日期為當天日期。\n可以直接使用 UnityCalendar.GetDate() 取得使用者設定日期，假如有錯誤會回報錯誤。\ntestGetDate.cs\npublic void OnClick_GetDate() { DateTime dt = unityCalendar.GetDate(); text.text = dt.ToString(\u0026#34;yyyy-MM-dd\u0026#34;); } public void OnClick_Clear() { text.text = string.Empty; unityCalendar.Init(); } Github ","date":"2021-06-17T00:00:00Z","permalink":"/posts/unity-calendar/","title":"Unity Calendar"},{"content":"前言 主要是實作 Addressable hotfix 的寫法。\n基本 Unity-Addressable 安裝及 Remote 設定可以參考這篇 Unity 筆記 Addressable Asset System。\n詳細解說可以參考\nUnity Addressables 深入浅出(一)、(二)、(三)\nUnity Addressable 獨立資源包\n實例 Initialization Initial Addressable System 是必要的假如不初始化會造成一些使用上的問題。\n在 Start 上可直接初始化 Addressable System\nIEnumerator Start() { var InitAddressablesAsync = Addressables.InitializeAsync(); yield return InitAddressablesAsync; } Update Catalog Catalog 是所有檔案的紀錄檔(log)，不更新 Catalog 也是能下載 Asset，可是會造成無法 hotfix，所以需要再下載前更一次 Catalog。\nCatalog Path 依 Window 為例，Catalog 更新後會放自動放置在\nC:\\Users\\[PC Name]\\AppData\\LocalLow\\[Company Name]\\[Product Name]\\com.unity.addressables\n名稱 解釋 [PC Name] 系統使用者名稱 [Company Name] Unity 專案 Company Name [Product Name] Unity 專案 Product Name IEnumerator UpdateCatalogCoro() { List\u0026lt;string\u0026gt; catalogsToUpdate = new List\u0026lt;string\u0026gt;(); var checkCatalogHandle = Addressables.CheckForCatalogUpdates(false); yield return checkCatalogHandle; if (checkCatalogHandle.Status == AsyncOperationStatus.Succeeded) catalogsToUpdate = checkCatalogHandle.Result; if (catalogsToUpdate.Count \u0026gt; 0) { var updateCatalogHandle = Addressables.UpdateCatalogs(catalogsToUpdate, false); yield return updateCatalogHandle; } } Update Asset 下載好的 Asset，在測試時不清除是會造成無法測試下載流程，可是可以手動清除下載 Asset。\nAsset Path 依 Window 為例，下載好的 Asset 更新後會放自動放置在\nC:\\Users\\[PC Name]\\AppData\\LocalLow\\Unity\\[Company Name]_[Product Name]\n名稱 解釋 [PC Name] 系統使用者名稱 [Company Name] Unity 專案 Company Name [Product Name] Unity 專案 Product Name Update All Asset 這個方式是使用 AA 系統紀錄 Catalog 取得出來的位置（Locator），在使用 GetDownloadSizeAsync 來確認檔案有無更新，來達成更新所有檔案。\nIEnumerator UpdateAllGroupsCoro() { foreach (var loc in Addressables.ResourceLocators) { foreach (var key in loc.Keys) { var sizeAsync = Addressables.GetDownloadSizeAsync(key); yield return sizeAsync; long totalDownloadSize = sizeAsync.Result; if (sizeAsync.Result \u0026gt; 0) { var downloadAsync = Addressables.DownloadDependenciesAsync(key); while (!downloadAsync.IsDone) { float percent = downloadAsync.PercentComplete; Debug.Log($\u0026#34;{key} = percent {(int)(totalDownloadSize * percent)}/{totalDownloadSize}\u0026#34;); yield return new WaitForEndOfFrame(); } Addressables.Release(downloadAsync); } Addressables.Release(sizeAsync); } } } Update label Asset 這個方式是使用 Label 下載特定資源。\nIEnumerator UpdateLabelAsset(string label) { long updateLabelSize = 0; var async = Addressables.GetDownloadSizeAsync(label); yield return async; if (async.Status == AsyncOperationStatus.Succeeded) updateLabelSize = async.Result; Addressables.Release(async); if (updateLabelSize == 0) { Debug.Log($\u0026#34;{label} last version\u0026#34;); yield break; } yield return DownloadLabelAsset(label); } IEnumerator DownloadLabelAsset(string label) { var downloadAsync = Addressables.DownloadDependenciesAsync(label, false); while (!downloadAsync.IsDone) { float percent = downloadAsync.PercentComplete; Debug.Log($\u0026#34;{label}: {downloadAsync.PercentComplete * 100} %\u0026#34;); yield return new WaitForEndOfFrame(); } Addressables.Release(downloadAsync); Debug.Log($\u0026#34;{label} UpdateAssets finish\u0026#34;); } Clear Asset 刪除 Asset Path 路徑下載的檔案。\nClear All Asset Caching.ClearCache() 是能夠完整的清除下載的所有檔案，但是不能單獨使用，還要搭配 Addressables.ClearDependencyCacheAsync 才能清除 Catalog 紀錄的下載資訊。\nIEnumerator ClearAllAssetCoro() { foreach (var locats in Addressables.ResourceLocators) { var async = Addressables.ClearDependencyCacheAsync(locats.Keys, false); yield return async; Addressables.Release(async); } Caching.ClearCache(); } Clear Label Asset IEnumerator ClearAssetCoro(string label) { var async = Addressables.LoadResourceLocationsAsync(label); yield return async; var locats = async.Result; foreach (var locat in locats) Addressables.ClearDependencyCacheAsync(locat.PrimaryKey); } Github 小結 write something cool\u0026hellip;\n參考連結 ","date":"2021-05-27T00:00:00Z","permalink":"/posts/unity-addressable/","title":"Unity Addressable"},{"content":"\nGitHub ","date":"2021-05-09T00:00:00Z","permalink":"/posts/unity-particlepath/","title":"Unity ParticlePath"},{"content":"前言 Unity 調整 Android、iOS 系統亮度功能。\n使用方式 Dimmer.SetBrightness.DoAction(value); Github 參考連結 Unity から iOS\u0026amp;Android の画面輝度を MAX にする方法\n","date":"2020-10-21T00:00:00Z","permalink":"/posts/unity-dimmer/","title":"Unity Dimmer"},{"content":"前言 在 UnityEngine.UI.Text 增加 TextSpacing，且調整 TextSpacing 的 Spacing 調整文字間格。\nScripts using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; [AddComponentMenu(\u0026#34;UI/Effects/TextSpacing\u0026#34;)] public class TextSpacing : BaseMeshEffect { #region Struct public enum HorizontalAligmentType { Left, Center, Right } public class Line { // 起點索引 public int StartVertexIndex { get { return _startVertexIndex; } } private int _startVertexIndex = 0; // 終點索引 public int EndVertexIndex { get { return _endVertexIndex; } } private int _endVertexIndex = 0; // 該行佔的點數目 public int VertexCount { get { return _vertexCount; } } private int _vertexCount = 0; public Line(int startVertexIndex, int length) { _startVertexIndex = startVertexIndex; _endVertexIndex = length * 6 - 1 + startVertexIndex; _vertexCount = length * 6; } } #endregion public float Spacing = 1f; public override void ModifyMesh(VertexHelper vh) { if (!IsActive() || vh.currentVertCount == 0) { return; } var text = GetComponent\u0026lt;Text\u0026gt;(); if (text == null) { Debug.LogError(\u0026#34;Missing Text component\u0026#34;); return; } // 水平對齊方式 HorizontalAligmentType alignment; if (text.alignment == TextAnchor.LowerLeft || text.alignment == TextAnchor.MiddleLeft || text.alignment == TextAnchor.UpperLeft) { alignment = HorizontalAligmentType.Left; } else if (text.alignment == TextAnchor.LowerCenter || text.alignment == TextAnchor.MiddleCenter || text.alignment == TextAnchor.UpperCenter) { alignment = HorizontalAligmentType.Center; } else { alignment = HorizontalAligmentType.Right; } var vertexs = new List\u0026lt;UIVertex\u0026gt;(); vh.GetUIVertexStream(vertexs); // var indexCount = vh.currentIndexCount; var lineTexts = text.text.Split(\u0026#39;\\n\u0026#39;); var lines = new Line[lineTexts.Length]; // 根據lines數組中各個元素的長度計算每一行中第一個點的索引，每個字、字母、空母均佔6個點 for (var i = 0; i \u0026lt; lines.Length; i++) { // 除最後一行外，vertexs對於前面幾行都有回車符佔了6個點 if (i == 0) { lines[i] = new Line(0, lineTexts[i].Length + 1); } else if (i \u0026gt; 0 \u0026amp;\u0026amp; i \u0026lt; lines.Length - 1) { lines[i] = new Line(lines[i - 1].EndVertexIndex + 1, lineTexts[i].Length + 1); } else { lines[i] = new Line(lines[i - 1].EndVertexIndex + 1, lineTexts[i].Length); } } UIVertex vt; for (var i = 0; i \u0026lt; lines.Length; i++) { for (var j = lines[i].StartVertexIndex; j \u0026lt;= lines[i].EndVertexIndex; j++) { if (j \u0026lt; 0 || j \u0026gt;= vertexs.Count) { continue; } vt = vertexs[j]; var charCount = lines[i].EndVertexIndex - lines[i].StartVertexIndex; if (i == lines.Length - 1) { charCount += 6; } if (alignment == HorizontalAligmentType.Left) { vt.position += new Vector3(Spacing * ((j - lines[i].StartVertexIndex) / 6), 0, 0); } else if (alignment == HorizontalAligmentType.Right) { vt.position += new Vector3(Spacing * (-(charCount - j + lines[i].StartVertexIndex) / 6 + 1), 0, 0); } else if (alignment == HorizontalAligmentType.Center) { var offset = (charCount / 6) % 2 == 0 ? 0.5f : 0f; vt.position += new Vector3(Spacing * ((j - lines[i].StartVertexIndex) / 6 - charCount / 12 + offset), 0, 0); } vertexs[j] = vt; // 以下注意點與索引的對應關係 if (j % 6 \u0026lt;= 2) { vh.SetUIVertex(vt, (j / 6) * 4 + j % 6); } if (j % 6 == 4) { vh.SetUIVertex(vt, (j / 6) * 4 + j % 6 - 1); } } } } } 問題 無法自由換行\n參考連結 UGUI 中 Text 的字间距\nUGUI 中随意调整 Text 中的字体间距\n","date":"2020-04-08T00:00:00Z","permalink":"/posts/add-text-spacing-for-unity/","title":"Add Text Spacing for Unity"},{"content":"前言 主要是用來測試虛擬搖桿功能。\n使用 Joystick Pack 支援各輸入端接口偛件。\nTutorial Github ","date":"2020-03-30T00:00:00Z","permalink":"/posts/mobile-joystick/","title":"Mobile Joystick"},{"content":"前言 視覺化管理使用路徑，不過目前功能還是很粗糙。\nFeature 視覺化管理\nUsage Create path 之後會在 Assets\\FolderManager\\StreamingAssets\\FolderManager.asset 出現 asset。\nAsset 即是FolderManager.Folders，因此可以直接宣告此 class 使用。\nGithub ","date":"2020-03-29T00:00:00Z","permalink":"/posts/folder-manager/","title":"Folder Manager"},{"content":"前言 Unity 在 WebGL 時存/讀檔案的方式，檔案是放在 IndexedDB。\nIndexedDB 是有容量大小限制，所以需要注意存儲的檔案大小。\nAbout IndexedDB Working with quota on mobile browsers\nGitHub Introduction File Path\nstring.Format(\u0026quot;{0}/{1}.dat\u0026quot;, Application.persistentDataPath, FileName);\nSave Method\nDataAccess.Save(fileName, bytes);\nLoad Method\nbyte[] bytes = DataAccess.Load(fileName);\nExample Scene\nroot\\Assets\\WebGL\\Example\\Scenes\\Example\n","date":"2020-02-27T00:00:00Z","permalink":"/posts/data-access-webgl/","title":"Data Access WebGL"},{"content":"前言 此為使用 Jenkins 輸出 Unity 專案注意事項。\nSetting 須注意 Unity 有無安裝輸出目標平台（Android、iOS、WebGL\u0026hellip;）。\n並且要設定 Jenkins 環境（AndroidSDK、JDK、Unity Editor）。\nJenkins Android SDK 需要新增 Jenkins 環境變數（Environment variable），來設定 Android SDK 路徑。\nJenkins 頁面路徑為 Manage Jenkins -\u0026gt; Configure System -\u0026gt; Global properties。\n設定如下圖：\nName：ANDROID_HOME\nValue：AndroidSDK 路徑。\nJenkins JDK JDK 版本請選 Java SE 8，因為 Unity 只支援 Java SE 8。\nJenkins 頁面路徑為 Manage Jenkins -\u0026gt; Global Tool Configuration -\u0026gt; JDK。\nJenkins Unity3d Plugin 需要至 Plugin Manager 安裝 Unity3d Plugin。\nJenkins 頁面路徑為 Manage Jenkins -\u0026gt; Plugin Manager -\u0026gt; Available\n安裝完成後，需要設定 Unity Editor 路徑。\nName：unity version\nInstallation directory：unity installed path\nJenkins item 基本設置可參考 使用 jenkins 建置 unity3d 專案 介紹。\n最主要是設定 Editor command line arguments。\n頁面路徑：Configure -\u0026gt; General -\u0026gt; Build\n點選 Add build step -\u0026gt; invoke Unity3d Editor，選擇對應的 Unity 編輯器版本。\n在 Editor command line arguments 輸入\n-projectPath \u0026#34;$WORKSPACE/\u0026#34; -executeMethod JenkinsBuild.BuildPlatforms -buildPath \u0026#34;$WORKSPACE\\Builds\u0026#34; -android -batchmode -nographics -quit -buildPath \u0026quot;$WORKSPACE\\Builds\u0026quot; \u0026ldquo;$WORKSPACE\\Builds 輸出放置資料夾路徑。\n-android 為輸出平台，可改為 -windows32、-windows64、-linux64、-macos、-android、-ios、-webgl。\nGitHub Repo reference 使用 jenkins 建置 unity3d 專案\nJenkins for Unity with DigitalOcean\n","date":"2020-02-25T00:00:00Z","permalink":"/posts/unity-jenkins-build/","title":"Unity Jenkins Build"},{"content":"前言 測試 LinkImageText 應用。\n主要內容 使用 QuickSheet 當資料庫，讓 FancyScrollView 顯示資料庫名字。\n點擊 FancyScrollView button 會在 HypeLinkText 顯示 name。\n點擊 HypeLinkText 超連結文字，會顯示超連結內容。\nGitHub repo ","date":"2020-02-24T00:00:00Z","permalink":"/posts/hyperlinks-in-unity-text/","title":"Hyperlinks in Unity Text"},{"content":"前言 前陣子因為專案需要後台推播功能，所以開始測試 FCM(Firebase Cloud Messaging）功能。\n測試的過程一直沒辦法在 APP 不活躍或不喚醒（not active）狀態推送推播訊息。\n後來發現主因是省電模式（Doze mode）導致 APP 無法接受任何推波內容。\n參考連結 https://developer.android.com/training/monitoring-device-state/doze-standby?hl=zh_cn\nhttps://blog.csdn.net/pkorochi/article/details/87186659\n","date":"2020-01-16T00:00:00Z","permalink":"/posts/fcm-notifications-not-received-on-android/","title":"FCM Notifications Not Received on Android"},{"content":"前言 在 Android 9.0 中使用 WebRequest 時，URL 是需要用 Https 才能正常使用，不然 Response 都是 Error。（Google Doc）\nError Log：Cleartext HTTP traffic to 45.xx.xxx.xx not permitted\nSolution 在 AndroidManifest.xml 的 application 加入 android:usesCleartextTraffic=\u0026quot;true\u0026quot;。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;manifest ...\u0026gt; \u0026lt;uses-permission android:name=\u0026#34;android.permission.INTERNET\u0026#34; /\u0026gt; \u0026lt;application ... android:usesCleartextTraffic=\u0026#34;true\u0026#34; ...\u0026gt; ... \u0026lt;/application\u0026gt; \u0026lt;/manifest\u0026gt; 參考連結 Android 中 HTTP 网络请求相关问题\n","date":"2020-01-09T00:00:00Z","permalink":"/posts/android-p-http-error/","title":"Android P HTTP Error"},{"content":"前言 主要用來 Unity app A 如何傳遞資訊給 Unity app B。\nDemo Script private class PropertyInfo { public string elementA = string.Empty; public string elementB = string.Empty; public string elementC = string.Empty; } public void Launch(string bundleId, string storelink) { bool fail = false; AndroidJavaClass up = new AndroidJavaClass(\u0026#34;com.unity3d.player.UnityPlayer\u0026#34;); AndroidJavaObject ca = up.GetStatic\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;currentActivity\u0026#34;); AndroidJavaObject packageManager = ca.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;getPackageManager\u0026#34;); AndroidJavaObject launchIntent = null; try { launchIntent = packageManager.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;getLaunchIntentForPackage\u0026#34;, bundleId); } catch (Exception e) { fail = true; } if (fail || launchIntent == null) Application.OpenURL(storelink); else { launchIntent.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;putExtra\u0026#34;, \u0026#34;elementA\u0026#34;, LaunchData.elementA); launchIntent.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;putExtra\u0026#34;, \u0026#34;elementB\u0026#34;, LaunchData.elementB); launchIntent.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;putExtra\u0026#34;, \u0026#34;elementC\u0026#34;, LaunchData.elementC); ca.Call(\u0026#34;startActivity\u0026#34;, launchIntent); } up.Dispose(); ca.Dispose(); packageManager.Dispose(); launchIntent.Dispose(); } Property PropertyInfo 是用來接受資訊的 class，這邊可以自行修改。\nGet Android intent Data for Unity\n","date":"2020-01-02T00:00:00Z","permalink":"/posts/launch-from-within-a-unity-app-another-unity-app-android/","title":"Launch From Within a Unity App Another Unity App Android"},{"content":"前言 主要用來 A App 呼叫 B App 時，B App 該如何接受資料。\n而 B App 是使用 Unity 接收。\nDemo Script private class PropertyInfo { public string elementA = string.Empty; public string elementB = string.Empty; public string elementC = string.Empty; } public class ExternalCall : MonoBehaviour { PropertyInfo info = new PropertyInfo(); private void Awake() { #if (!UNITY_EDITOR \u0026amp;\u0026amp; UNITY_ANDROID) CreatePushClass(new AndroidJavaClass(\u0026#34;com.unity3d.player.UnityPlayer\u0026#34;)); #endif } public void CreatePushClass(AndroidJavaClass UnityPlayer) { #if UNITY_ANDROID AndroidJavaObject currentActivity = UnityPlayer.GetStatic\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;currentActivity\u0026#34;); AndroidJavaObject intent = currentActivity.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;getIntent\u0026#34;); bool elementA_hasExtra = IsBool(intent, \u0026#34;elementA\u0026#34;); bool elementB_hasExtra = IsBool(intent, \u0026#34;elementB\u0026#34;); bool elementC_hasExtra = IsBool(intent, \u0026#34;elementC\u0026#34;); AndroidJavaObject extras = GetExtras(intent); if (extras != null) { if (elementA_hasExtra) info.elementA = GetProperty(extras, \u0026#34;elementA\u0026#34;); if (elementB_hasExtra) info.elementB = GetProperty(extras, \u0026#34;elementB\u0026#34;); if (elementC_hasExtra) info.elementC = GetProperty(extras, \u0026#34;elementC\u0026#34;); } #endif } private bool IsBool(AndroidJavaObject intent, string method) { bool b = false; try { b = intent.Call\u0026lt;bool\u0026gt;(\u0026#34;hasExtra\u0026#34;, method); } catch (Exception e) { Debug.Log(e.Message); } return b; } private AndroidJavaObject GetExtras(AndroidJavaObject intent) { AndroidJavaObject extras = null; try { extras = intent.Call\u0026lt;AndroidJavaObject\u0026gt;(\u0026#34;getExtras\u0026#34;); } catch (Exception e) { Debug.Log(e.Message); } return extras; } private string GetProperty(AndroidJavaObject extras, string name) { string s = string.Empty; try { s = extras.Call\u0026lt;string\u0026gt;(\u0026#34;getString\u0026#34;, name); } catch (Exception e) { Debug.Log(e.Message); } return s; } } Property PropertyInfo 是用來接受資訊的 class，這邊可以自行修改。\nLaunch from within a Unity app another Unity app(Android)\n","date":"2019-12-26T00:00:00Z","permalink":"/posts/get-android-intent-data-for-unity/","title":"Get Android Intent Data for Unity"},{"content":"Install 去官網下載 Inno Setup，請下載 Stable Release 版本。\nAdd Language 下載官方提供的 Github，直接下載 Releases 版本，完成後解壓縮。 複製 root/Files/Languages 資料夾，貼上並且覆蓋 Inno Setup 安裝資料夾。 使用方式 可以參考以下腳本，也可以自己寫，不知道寫法可以參考官方文件。\n#define MyAppGUID \u0026#34;{{D0D7EBDD-2493-4086-A306-AB012D2AFA93}\u0026#34; #define MyAppName \u0026#34;Examle\u0026#34; #define MyAppFolder \u0026#34;ExampleFolder\u0026#34; #define MyAppSetupExeName \u0026#34;Examle\u0026#34; #define MyAppExeName \u0026#34;Examle.exe\u0026#34; #define MyAppURL \u0026#34;https://wenrongdev.com/\u0026#34; #define MyAppPublisher \u0026#34;wen rong studio\u0026#34; [Setup] AppId={#MyAppGUID} AppName={#MyAppName} AppVersion=0.1.0 AppVerName={#MyAppName} AppPublisher = {#MyAppPublisher} AppPublisherURL = {#MyAppURL} AppSupportURL = {#MyAppURL} AppUpdatesURL = {#MyAppURL} Compression = lzma2 DefaultDirName={commonpf32}\\{#MyAppFolder} DisableProgramGroupPage=yes DefaultGroupName={#MyAppName} UninstallDisplayIcon={app}ForwardSlash{#MyAppExeName} SolidCompression = no OutputDir = \u0026#34;Setup\u0026#34; OutputBaseFilename = {#MyAppSetupExeName} ShowLanguageDialog=yes // 是否需要分割 DiskSpanning=yes SlicesPerDisk=3 DiskSliceSize=1566000000 /// [Languages] Name: EN; MessagesFile: \u0026#34;compiler:Default.isl\u0026#34; Name: CT; MessagesFile: \u0026#34;compiler:Languages\\Unofficial\\ChineseTraditional.isl\u0026#34; Name: CS; MessagesFile: \u0026#34;compiler:Languages\\Unofficial\\ChineseSimplified.isl\u0026#34; Name: JP; MessagesFile: \u0026#34;compiler:Languages\\Japanese.isl\u0026#34; [CustomMessages] MyAppName = {#MyAppName} MyAppVerName = {#MyAppName} %1 [Messages] BeveledLabel = {#MyAppURL} [Dirs] Name: \u0026#34;{app}\u0026#34;; Permissions: everyone-full [Files] Source: \u0026#34;{#MyAppFolder}\\*\u0026#34;; DestDir: \u0026#34;{app}\\{#MyAppFolder}\u0026#34;; Flags: ignoreversion recursesubdirs [Icons] Name: \u0026#34;{userdesktop}\\{cm:MyAppName}\u0026#34;; Filename: \u0026#34;{app}\\{#MyAppFolder}\\{#MyAppExeName}\u0026#34;; [Code] function GetNumber(var temp: String): Integer; var part: String; pos1: Integer; begin if Length(temp) = 0 then begin Result := -1; Exit; end; pos1 := Pos(\u0026#39;.\u0026#39;, temp); if (pos1 = 0) then begin Result := StrToInt(temp); temp := \u0026#39;\u0026#39;; end else begin part := Copy(temp, 1, pos1 - 1); temp := Copy(temp, pos1 + 1, Length(temp)); Result := StrToInt(part); end; end; function CompareInner(var temp1, temp2: String): Integer; var num1, num2: Integer; begin num1 := GetNumber(temp1); num2 := GetNumber(temp2); if (num1 = -1) or (num2 = -1) then begin Result := 0; Exit; end; if (num1 \u0026gt; num2) then begin Result := 1; end else if (num1 \u0026lt; num2) then begin Result := -1; end else begin Result := CompareInner(temp1, temp2); end; end; function CompareVersion(str1, str2: String): Integer; var temp1, temp2: String; begin temp1 := str1; temp2 := str2; Result := CompareInner(temp1, temp2); end; function InitializeSetup(): Boolean; var oldVersion: String; uninstaller: String; ErrorCode: Integer; vCurID :String; vCurAppName :String; begin vCurID:= \u0026#39;{#SetupSetting(\u0026#34;AppId\u0026#34;)}\u0026#39;; vCurAppName:= \u0026#39;{#SetupSetting(\u0026#34;AppName\u0026#34;)}\u0026#39;; vCurID:= Copy(vCurID, 2, Length(vCurID) - 1); if RegKeyExists(HKEY_LOCAL_MACHINE, \u0026#39;SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\u0026#39; + vCurID + \u0026#39;_is1\u0026#39;) then begin RegQueryStringValue(HKEY_LOCAL_MACHINE, \u0026#39;SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\u0026#39; + vCurID + \u0026#39;_is1\u0026#39;, \u0026#39;DisplayVersion\u0026#39;, oldVersion); if (CompareVersion(oldVersion, \u0026#39;{#SetupSetting(\u0026#34;AppVersion\u0026#34;)}\u0026#39;) \u0026lt; 0) then begin if MsgBox(\u0026#39;Version \u0026#39; + oldVersion + \u0026#39; of \u0026#39; + vCurAppName + \u0026#39; is already installed. Continue to use this old version?\u0026#39;, mbConfirmation, MB_YESNO) = IDYES then begin Result := False; end else begin RegQueryStringValue(HKEY_LOCAL_MACHINE, \u0026#39;SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\u0026#39; + vCurID + \u0026#39;_is1\u0026#39;, \u0026#39;UninstallString\u0026#39;, uninstaller); ShellExec(\u0026#39;runas\u0026#39;, uninstaller, \u0026#39;/SILENT\u0026#39;, \u0026#39;\u0026#39;, SW_HIDE, ewWaitUntilTerminated, ErrorCode); Result := True; end; end else begin MsgBox(\u0026#39;Version \u0026#39; + oldVersion + \u0026#39; of \u0026#39; + vCurAppName + \u0026#39; is already installed. This installer will exit.\u0026#39;, mbInformation, MB_OK); Result := False; end; end else begin Result := True; end; end; 修改方式 假如是使用複製舊有的 .iss 檔，只需要修改幾個需要注意的文字即可。\n正常的資料夾結構\nFolder Description ExampleFolder 打包前的資料夾 Setup Inno Setup 輸出的資料夾 .iss Inno Setup Script 修改 .iss #define MyAppGUID \u0026#34;GUID\u0026#34; #define MyAppName \u0026#34;Examle\u0026#34; #define MyAppFolder \u0026#34;ExampleFolder\u0026#34; #define MyAppSetupExeName \u0026#34;Examle\u0026#34; #define MyAppExeName \u0026#34;Examle.exe\u0026#34; #define MyAppURL \u0026#34;https://wenrongdev.com/\u0026#34; #define MyAppPublisher \u0026#34;wen rong studio\u0026#34; 需修改地方 Arg Description MyAppGUID 安裝系統 GUID，產生方式為 Tools/Generated GUID。 MyAppName 桌面路徑名稱 。 MyAppFolder 安裝目錄名稱。 MyAppSetupExeName Inno Setup 輸出安裝檔名稱 。 取得 GUID 方法 Github ","date":"2019-12-19T00:00:00Z","permalink":"/posts/example-inno-setup/","title":"Example Inno Setup"},{"content":"前言 因為某些程式開啟時，會跳出需要系統管理員（Administrator）權限執行程式，也導致了只要是ㄧ般使用者每次開啟時都需要輸入系統管理員密碼來執行。為了ㄧ般使用者的權限問題也不能關閉 Windows UAC。 也不可能修改一般使用者的權限，所以需要讓 Windows UAC 加入此程式為白名單，這樣就不會每次都會跳出權限要求。\n已知限制條件 程式必須以 Administrator 執行 一般使用者可以執行 不可完全關閉 Windows UAC 解決方式 根據不變更 UAC 安全性，但執行程式時又不擾民的設定方式，可以在 Windows Regedit 新增白名單。\nWin+R 輸入 regedit 執行 根據此路徑尋找 HKEY_CURRENT_USERS\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers 右鍵新增字串值，名稱為程式（exe）路徑、資料為~ RunAsInvoker C# 解決方式 public class RegEditWhiteList { public string keyName { get; set; } private readonly string root = @\u0026#34;Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\u0026#34;; private readonly string keyValue = \u0026#34;~ RunAsInvoker\u0026#34;; public RegEditWhiteList(string keyName) { this.keyName = keyName; } public void SendRegedit() { RegistryKey key = Registry.CurrentUser.OpenSubKey(root, true); key.SetValue(keyName, keyValue,RegistryValueKind.String); key.Close(); } } GitHub 使用方式 因為修改註冊碼是修正當前使用者的註冊碼，因此只要換使用者就需要再新增一次白名單。\n參考資料 不變更 UAC 安全性，但執行程式時又不擾民的設定方式\n","date":"2019-11-04T00:00:00Z","permalink":"/posts/whitelist-for-windows-uac/","title":"Whitelist for Windows UAC"},{"content":"前言 Unity 2019.2.11f1 Vuforia 8.5.9 iOS Build and Run Error ld: library not found for -liPhone-lib clang: error: linker command failed with exit code 1 (use -v to see invocation) Solution - iOS Build and Run Error Build Setting -\u0026gt; Search Paths -\u0026gt; Library Search Paths\n移除 \u0026quot;$(SECROOT)\u0026quot; 參數\niOS Archive Error ld: warning: ignoring file ... building for iOS-armv7 but attempting to link with file built for iOS-arm64. Solution - iOS Archive Error 根據 Vuforia Engine Release Notes 在 v8.1.7之後不支援 32-bit，並且最低支援 iOS 11，因此需要把專案版本最低版本設定為 iOS。\n設定 iOS architectures\nBuild Setting -\u0026gt; Architectures -\u0026gt; Architectures\nArchitectures 改為 Standard architectures\n設定 iOS 版本\nBuild Setting -\u0026gt; Deployment -\u0026gt; iOS Deployment Target\niOS Deployment Target 改為 iOS 11.0\niOS Distribution Error ERROR ITMS-90534 Solution - iOS Distribution Error 請使用 Xcode 11.2.1 輸出，即可修正。\n參考文章 Unity Xcode Error \u0026ldquo;library not found for -\u0026rdquo; の解決方法\nCan\u0026rsquo;t submit apps to AppStore: ERROR ITMS-90534: \u0026ldquo;Invalid Toolchain\n","date":"2019-10-17T00:00:00Z","permalink":"/posts/ios-build-note-for-unity-2019-vuforia/","title":"IOS Build Note for Unity 2019 Vuforia"},{"content":"前言 由於 iTunes 12.6 之後不提供 .ipa 檔安裝，導致無法提供測試 App，所以有人研究出很多安裝方式。不過這邊主要是介紹 OTA 的方式。\n其他的安裝方式：iTunes 12.7 移除了 Apps 的選項，我該如何安裝 .ipa 檔案到 iOS 裝置？\nUsing OTA 使用 OTA（Over-the-Air）需要有三個檔案 .ipa(ad-Hoc)、.plist、index.html\n.ipa(ad-Hoc) 需要上傳去雲端空間，目前選擇的雲端空間是 Dropbox。\n還需要 Host Website 上傳 .plist、index.html，目前選擇的是 GitHub。\n所以只要使用 Host Website 就可以安裝了。\n參考文章：Error when distributing an IPA over the air with dropbox - iOS 7.1\nUpload .ipa to Dropbox and Get public link 先把輸出好的 .ipa(ad-Hoc) 上傳至 Dropbox 並且設定分享。\n將分享網址裡的 www.dropbox.com 替換為 dl.dropboxusercontent.com。\n紀錄修改 public link。\nCreate manifest.plist \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;items\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;assets\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;kind\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;software-package\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;url\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;http://YOUR_SERVER_URL/YOUR-IPA-FILE.ipa\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;metadata\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;bundle-identifier\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;com.yourCompany.productName\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;bundle-version\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;1.0.0\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;kind\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;software\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;title\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;YOUR APP NAME\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 修改好 public link、app info 就完成 .plist。\nCreate index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;App install\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h2\u0026gt;builds\u0026lt;/h2\u0026gt; \u0026lt;a href=\u0026#34;itms-services://?action=download-manifest\u0026amp;url=http://YOUR_SERVER_URL/manifest.plist\u0026#34;\u0026gt; App\u0026lt;/a\u0026gt;\u0026lt;/body\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 把 url=http://YOUR_SERVER_URL/manifest.plist 替換成 Website manifest.plist。\nHosting using Github Create a new repository（教學文章） 上傳 manifest.plist、index.html 得到 Github Website index.html就可以安裝 .ipa\n參考文章 iTunes 12.7 移除了 Apps 的選項，我該如何安裝 .ipa 檔案到 iOS 裝置？\n從網路上下載 ipa 檔並且安裝在 iPhone or iPad(Download and install an ipa from url on iOS)\nError when distributing an IPA over the air with dropbox - iOS 7.1\n使用 GitHub 免費製作個人網站\n","date":"2019-10-10T00:00:00Z","permalink":"/posts/install-ipa-with-ota/","title":"Install IPA With OTA"},{"content":"前言 在 Unity API 中有 Application.systemLanguage 可以取得系統語言。可是 Unity 5.3 用於 iOS 上，只要是裝置為中文語系一律回傳 SystemLanguage.Chinese，無法判別簡 / 繁語系，因此才研究怎麼取得 Windows、Android、iOS 原生語系。\nWindows Platform 利用 GetSystemDefaultLCID 取得本機端語系，再利用 CultureInfo.GetCultureInfo 轉化為本機端語系文化。\n[DllImport(\u0026#34;kernel32.dll\u0026#34;)] 參考文章\nAndroid Platform 直接呼叫原生系統 API 取得。\nprivate static string CurrentAndroidLanguage() { string result = \u0026#34;\u0026#34;; using (AndroidJavaClass cls = new AndroidJavaClass(\u0026#34;java.util.Locale\u0026#34;)) { if (cls != null) { using (AndroidJavaObject locale = cls.CallStatic(\u0026#34;getDefault\u0026#34;)) { if (locale != null) { result = locale.Call(\u0026#34;getLanguage\u0026#34;) + \u0026#34;_\u0026#34; + locale.Call(\u0026#34;getDefault\u0026#34;); Debug.Log(\u0026#34;Android lang: \u0026#34; + result); } else { Debug.Log(\u0026#34;locale null\u0026#34;); } } } else { Debug.Log(\u0026#34;cls null\u0026#34;); } } return result; } 參考文章\niOS Platfrom 製作一個 .mm 文件，內容如下\nchar* cStringCopy(const char* string) { if(string == NULL){ return NULL; } char* newString = (char*)malloc(strlen(string) + 1); strcpy(newString, string); return newString; } extern \u0026#34;C\u0026#34; { const char* CurIOSLang () { NSArray *languages = [NSLocale preferredLanguages]; NSString *CurrentLanguage = [languages objectAtIndex:0]; return cStringCopy([CurrentLanguage UTF8String]); } } 在 C# 寫出 CurIOSLang 的接口\n[DllImport(\u0026#34;__Internal\u0026#34;)] private static extern string CurIOSLang(); 這樣就可以在 Unity 直接呼叫 CurIOSLan 取得 iOS 語系了。\n參考文章\nGithub repo ","date":"2019-10-04T00:00:00Z","permalink":"/posts/unity-5.3-native-system-language/","title":"Unity 5.3 Native System Language"},{"content":"前言 執行 WebGL 時都會有 Unity Logo \u0026amp; Loading。目前此專案修改 Unity Logo 的部分。\n需要更詳細的內容可以參考官方文件（Unity Document）。\n會比較建議使用 Responsive WebGL Template，省去自己測試修改的麻煩，不過還是需要改 Logo、Icon 的部分。\nSetting Up Your Template Import Unitypackage\nSet up Unity Player Setting Edit -\u0026gt; Project Settings -\u0026gt; Player, On the WebGL tab -\u0026gt; Resolution and Presentation -\u0026gt; Selcet LogoTemplates\nChange Your Logo\nLogo 規格建議不要太大張。\nPath: root/Assets/WebGLTemplates/LogoTemplate/logo.png\nGitHub repo 參考文章\n","date":"2019-09-05T00:00:00Z","permalink":"/posts/unity-webgl-template/","title":"Unity WebGL Template"},{"content":"前言 此 Script 用於 WebGL RectMask2D 失去作用的簡易修正。\n建議還是先輸出測試確定 RectMask2D 失效再使用此 Script。\n使用方式 直接在 Canvas 物件底下 Add Component FixRectMask2dWebGL 即可。\nScript public class FixRectMask2dWebGL : MonoBehaviour { #if PlatformWebGL private void Awake() { var items = GetComponentsInChildren\u0026lt;MaskableGraphic\u0026gt;(true); for (int i = 0; i \u0026lt; items.Length; i++) { Material m = items[i].materialForRendering; if (m != null) m.EnableKeyword(\u0026#34;UNITY_UI_CLIP_RECT\u0026#34;); } } #endif } 參考文章\n","date":"2019-08-29T00:00:00Z","permalink":"/posts/unity-webgl-rectmask2d-does-not-work/","title":"Unity WebGL RectMask2D Does Not Work"}]