LINQ小技巧 - 避免WMI的"Not Found"例外

LINQ 的設計初衷就是放在程式設計師日常都會用到的功能上,藉此節省程式碼的量,身為C# 工程師,我們在解搜尋、排序、過濾這些問題時,應該時時刻刻都要想到如果運用LINQ,是否會有更好的解法,
本系列文便是一個紀錄,我沒有定任何的週期發布時間,純屬遇到後,就記錄下來,也不代表是最好的解法,你知道,這世界上只有當下最佳解,如果你有更好的解法,歡迎分享 ^_^ ,若對這系列有興趣,可以追蹤本粉絲頁。

WMI 篇

  WMI在Windows系統是很方便的機制,可以讓我們取得許多的硬體資訊或是系統資訊,例如硬碟、顯示卡、行程等資訊,以下是一個列出目前系統中所有磁碟大小的例子。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive"))
            {
                foreach (var item in searcher.Get())
                {
                    Console.WriteLine($"Size: {item["Size"]}");
                }
            }
            Console.ReadLine();
        }
    }
}

使用時記得加入System.Management這個Assembly做為Reference,這是一個可正常運作的例子。但你知道嗎?WMI所回傳的資訊有可能因作業系統的版本而有所差異(此例是不會),例如記憶體資訊在Windows 10和Windows 7回傳的屬性數量就不同了,

如果遭遇這種情況,WMI會毫不留情地拋出一個例外。

那麼怎麼避免這個例外,在找不到這個屬性時,給個預設值,讓程式免於Crash的窘境了?最簡單的方式就是try...catch。

foreach (var item in searcher.Get())
{
   try
   {
      Console.WriteLine($"Size: {item["Size1"]}");
   }
   catch(ManagementException ex)
   {
      //do some log.
      Console.WriteLine("Size: not found");
   }
}

有效,雖然例外並不是什麼大事,但能避免的話是最好,因為畢竟例外會多耗費一些處理資源,除此之外,如果要處理的屬性多時,難免出現一堆的try...catch區段,運用Extension Method大概是常見的手法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp9
{
    public static class ManagementHelper
    {
        public static object GetValueOrDefault(this ManagementBaseObject source, string key)
        {
            try
            {
                return source[key];
            }
            catch(ManagementException)
            {
            }
            return null;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive"))
            {
                foreach (var item in searcher.Get())
                {
                    Console.WriteLine($"Size: {item.GetValueOrDefault("Size") ?? "not found"}");
                }
            }
            Console.ReadLine();
        }
    }
}

比起包一堆的try...catch,看起來好多了是吧?結案。 啊? 這跟LINQ有啥關係?喔,我忘了自己正在寫LINQ的主題.....................

 

 

 

好吧,假設如果能運用LINQ的Where及FirstOrDefault那不是很好,可惜,這兩個函式並不能用在這。

此時不妨深究一下item的型別。

接著看一下ManagementBaseObject的定義。

事實上,["key"]是Properties這個Collection屬性的捷徑,看一下PropetyDataCollection的定義。

只要看到ICollection、IEnumerable,那麼就代表著可能可以運用LINQ來解這個問題了,可是還是有問題,因為就算使用Properties,還是無法使用Where,因為Where、FirstOrDefault是IEnumerable<T>的Extension Method。

不過你應該看出端倪了,裡面出現了Cast,雖然Cast強制轉型可能不是很安全的做法,但是另一個OfType可是非常安全,因此可以寫下以下的程式碼。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive"))
            {
                foreach (var item in searcher.Get())
                {
                    Console.WriteLine($@"Size: {item.Properties.OfType<PropertyData>().FirstOrDefault(
                                              a => a.Name == "Size")?.Value ?? "not found"}");
                }
            }
            Console.ReadLine();
        }
    }
}

這裡用了點小技巧,真實使用時,你可能還要處理轉型及Log的問題(找不到就是要Log),不過我們確實避開了這個例外,Cast與OfType是可以用在所有實作IEnumerable介面的物件上,活用他們可以把本來受限於非IEnumerable<T>的物件帶往LINQ豐富功能的領域。

當然,這個例子可能在大量使用時會有效能問題(例如你要列出一個以上的屬性),此時可以運用ToDictionary來處理。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive"))
            {
                foreach (var item in searcher.Get())
                {
                    var props = item.Properties.OfType<PropertyData>().ToDictionary(a => a.Name);
                    Console.WriteLine($"Size: { (props.ContainsKey("Size") ? props["Size"].Value : "not found") }");
                }
            }
            Console.ReadLine();
        }
    }
}

美中不足的是這裡依然需要針對找不到鍵值的情況使用三元式,但比起上例每個屬性都where一次好太多了。

就效能上來說,如果屬性正確,最早的Exception處理效能最好,但是一旦出現例外,那就不一定了,寫程式就是在效能跟易用性上做抉擇,端看你要的是哪塊。