C#基礎篇——泛型

前言

在開發編程中,咱們常常會遇到功能很是類似的功能模塊,只是他們的處理的數據不同,因此咱們會分別採用多個方法來處理不一樣的數據類型。可是這個時候,咱們就會想一個問題,有沒有辦法實現利用同一個方法來傳遞不一樣種類型的參數呢?編程

這個時候,泛型也就因運而生,專門來解決這個問題的。c#

泛型是在C#2.0就推出的一個新語法,由框架升級提供的功能。設計模式

說明

泛型經過參數化類型實如今同一份代碼上操做多種數據類型。例如使用泛型的類型參數T,定義一個類Stack 數組

能夠用Stack 、Stack 或者Stack 實例化它,從而使類Stack能夠處理int、string、Person類型數據。這樣能夠避免運行時類型轉換或封箱操做的代價和風險。泛型提醒的是將具體的東西模糊化。 緩存

同時使用泛型類型能夠最大限度地重用代碼、保護類型安全以及提升性能。安全

能夠建立:泛型接口泛型類泛型方法泛型事件泛型委託框架

開始

泛型類

泛型類封裝不特定於特定數據類型的操做。 泛型類最多見用法是用於連接列表、哈希表、堆棧、隊列和樹等集合。 不管存儲數據的類型如何,添加項和從集合刪除項等操做的執行方式基本相同。ide

static void Main(string[] args)
    {

        // T是int類型
        GenericClass<int> genericInt = new GenericClass<int>();
        genericInt._T = 123;
        // T是string類型
        GenericClass<string> genericString = new GenericClass<string>();
        genericString._T = "123";

    }

新建一個GenericClass類函數

/// <summary>
    /// 泛型類
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class GenericClass<T>
    {
        public T _T;
    }

泛型方法

泛型方法是經過類型參數聲明的方法, 解決用一個方法,知足不一樣參數類型性能

static void Main(string[] args)
    {
        #region 泛型方法
        Console.WriteLine("************Generic**************");
        int iValue = 123;
        string sValue = "456";
        DateTime dtValue = DateTime.Now;
        object oValue = "MrValue";
        GenericMethod.Show<int>(iValue);//須要指定類型參數
        //GenericMethod.Show<string>(iValue);//必須吻合
        GenericMethod.Show(iValue);//能省略,自動推算
        GenericMethod.Show<string>(sValue);
        GenericMethod.Show<DateTime>(dtValue);
        GenericMethod.Show<object>(oValue);
        #endregion

    }

新建一個GenericMethod

/// <summary>
/// 泛型方法
/// </summary>
public class GenericMethod
{
    /// <summary>
    /// 2.0推出的新語法
    /// 泛型方法解決用一個方法,知足不一樣參數類型;作相同的事兒
    /// 沒有寫死參數類型,調用的時候才指定的類型
    /// 延遲聲明:把參數類型的聲明推遲到調用
    /// 推遲一切能夠推遲的~~  延遲思想
    /// 不是語法糖,而是2.0由框架升級提供的功能
    /// 須要編譯器支持+JIT支持
    /// </summary>
    /// <typeparam name="T">T/S 不要用關鍵字  也不要跟別的類型衝突 </typeparam>
    /// <param name="tParameter"></param>
    public static void Show<T>(T tParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",
            typeof(GenericMethod), tParameter.GetType().Name, tParameter.ToString());
    }
}

泛型接口

爲泛型集合類或表示集合中的項的泛型類定義接口一般頗有用處。在c#中,經過尖括號「<>」將類型參數括起來,表示泛型。聲明泛型接口時,與聲明通常接口的惟一區別是增長了一個 。通常來講,聲明泛型接口與聲明非泛型接口遵循相同的規則。

泛型接口定義完成以後,就要定義此接口的子類。定義泛型接口的子類有如下兩種方法。

(1)直接在子類後聲明泛型。

(2)在子類實現的接口中明確的給出泛型類型。

static void Main(string[] args)
    {
        #region 泛型接口
        CommonInterface commonInterface = new CommonInterface();
        commonInterface.GetT("123");
        #endregion
    }

新建GenericInterface.cs類文件

/// <summary>
        /// 泛型類
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class GenericClass<T>
        {
            public T _T;
        }

        /// <summary>
        /// 泛型接口
        /// </summary>
        public interface IGenericInterface<T>
        {
            //泛型類型的返回值
            T GetT(T t);
        }


        /// <summary>
        /// 使用泛型的時候必須指定具體類型,
        /// 這裏的具體類型是int
        /// </summary>
        public class CommonClass : GenericClass<int>
        {

        }

        /// <summary>
        /// 必須指定具體類型
        /// </summary>
        public class CommonInterface : IGenericInterface<string>
        {
            public string GetT(string t)
            {
                return t;
            }
        }

        /// <summary>
        /// 子類也是泛型的,繼承的時候能夠不指定具體類型
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class CommonClassChild<T> : GenericClass<T>
        {

        }

泛型委託

泛型委託主要是想講一下Action 和Func 兩個委託,由於這兩個在Linq中是常常見到的。

Action 只能委託必須是無返回值的方法

Fun 只是委託必須有返回值的方法

無論是否是泛型委託,只要是委託委託那能用Lamdba表達式,由於無論Lamdba表達式仍是匿名函數其實都是將函數變量化。

下面簡單的來作的demo說下兩個的用法,這個會了基本linq會了一半了。

static void Main(string[] args)
    {
        #region 泛型委託
        Action<string> action = s => {
            Console.WriteLine(s);
        };
        action("i3yuan");
        Func<int, int, int> func = (int a, int b) => {
            return a + b;
        };
        Console.WriteLine("sum:{0}", func(1,1));
        Console.ReadLine();
        #endregion
    }

上面其實都是將函數作爲變量,這也是委託的思想。action是實例化了一個只有一個字符串參數沒有返回值得函數變量。func是實例化了一個有兩個int類型的參數返回值爲int的函數變量。

能夠看到經過Lamdba表達式和泛型的結合,算是又方便了開發者們,更加方便實用。

引入委託經常使用的另外一方式

不管是在類定義內仍是類定義外,委託能夠定義本身的類型參數。引用泛型委託的代碼能夠指定類型參數來建立一個封閉構造類型,這和實例化泛型類或調用泛型方法同樣,以下例所示:

public delegate void MyDelegate<T>(T item);
public void Notify(int i){}
//...
 
MyDelegate<int> m = new MyDelegate<int>(Notify);
 
C#2.0版有個新特性稱爲方法組轉換(method group conversion),具體代理和泛型代理類型均可以使用。用方法組轉換能夠把上面一行寫作簡化語法:
MyDelegate<int> m = Notify;
 
在泛型類中定義的委託,能夠與類的方法同樣地使用泛型類的類型參數。
class Stack<T>
{
T[] items;
      int index
//...
public delegate void StackDelegate(T[] items);
}
 
引用委託的代碼必需要指定所在類的類型參數,以下:
 
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate myDelegate = StackNotify;
 
 
泛型委託在定義基於典型設計模式的事件時特別有用。由於sender[JX2] ,而不再用與Object相互轉換。
public void StackEventHandler<T,U>(T sender, U eventArgs);
class Stack<T>
{
    //…
    public class StackEventArgs : EventArgs{...}
    public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
    protected virtual void OnStackChanged(StackEventArgs a)
    {
      stackEvent(this, a);
    }
}
class MyClass
{
  public static void HandleStackChange<T>(Stack<T> stack, StackEventArgs args){...};
}
Stack<double> s = new Stack<double>();
MyClass mc = new MyClass();
s.StackEventHandler += mc.HandleStackChange;

泛型約束

所謂的泛型約束,實際上就是約束的類型T。使T必須遵循必定的規則。好比T必須繼承自某個類,或者T必須實現某個接口等等。那麼怎麼給泛型指定約束?其實也很簡單,只須要where關鍵字,加上約束的條件。

定義一個People類,裏面有屬性和方法:

public interface ISports
    {
        void Pingpang();
    }
    public interface IWork
    {
        void Work();
    }
    public class People
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
    
    }
    public class Chinese : People, ISports, IWork
    {
        public void Tradition()
        {
            Console.WriteLine("仁義禮智信,溫良恭儉讓");
        }
        public void SayHi()
        {
            Console.WriteLine("吃了麼?");
        }
    
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    
        public void Work()
        {
            throw new NotImplementedException();
        }
    } 
    public class Hubei : Chinese
    {
        public Hubei(int version)
        { }
    
        public string Changjiang { get; set; }
        public void Majiang()
        {
            Console.WriteLine("打麻將啦。。");
        }
    }
    public class Japanese : ISports
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    }

打印方法

/// <summary>
    /// 打印個object值
    /// 1 object類型是一切類型的父類
    /// 2 經過繼承,子類擁有父類的一切屬性和行爲;任何父類出現的地方,均可以用子類來代替
    /// object引用類型  加入傳個值類型int  會有裝箱拆箱  性能損失
    /// 類型不安全
    /// </summary>
    /// <param name="oParameter"></param>
    public static void ShowObject(object oParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",
            typeof(Constraint), oParameter.GetType().Name, oParameter);

        Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}");

    }

在main方法中

static void Main(string[] args)
    {
        #region  Constraint 接口約束
        Console.WriteLine("************Constraint*****************");
        {
            People people = new People()
            {
                Id = 123,
                Name = "走本身的路"
            };
            Chinese chinese = new Chinese()
            {
                Id = 234,
                Name = "晴天"
            };
            Hubei hubei = new Hubei(123)
            {
                Id = 345,
                Name = "流年"
            };
            Japanese japanese = new Japanese()
            {
                Id = 7654,
                Name = "i3yuan"//
            };
            CommonMethod.ShowObject(people);
            CommonMethod.ShowObject(chinese);
            CommonMethod.ShowObject(hubei);
            CommonMethod.ShowObject(japanese);
  
            Console.ReadLine();
        }
        #endregion
    }

泛型約束總共有五種。

約束 說明
T:結構 類型參數必須是值類型
T:類 類型參數必須是引用類型;這一點也適用於任何類、接口、委託或數組類型。
T:new() 類型參數必須具備無參數的公共構造函數。 當與其餘約束一塊兒使用時,new() 約束必須最後指定。
T:<基類名> 類型參數必須是指定的基類或派生自指定的基類。
T:<接口名稱> 類型參數必須是指定的接口或實現指定的接口。 能夠指定多個接口約束。 約束接口也能夠是泛型的。

一、基類約束

上面打印的方法約束T類型必須是People類型。

///


/// 基類約束:約束T必須是People類型或者是People的子類
/// 1 可使用基類的一切屬性方法---權利
/// 2 強制保證T必定是People或者People的子類---義務

/// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter) where T : People
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
        }

注意:

基類約束時,基類不能是密封類,即不能是sealed類。sealed類表示該類不能被繼承,在這裏用做約束就無任何意義,由於sealed類沒有子類。

二、接口約束

/// <summary>
        /// 接口約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : ISports
        {
            t.Pingpang();
            return t;
        }

三、引用類型約束 class

引用類型約束保證T必定是引用類型的。

/// 引用類型約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : class
        {
            return t;
        }

四、值類型約束 struct

值類型約束保證T必定是值類型的。

/// 值類型類型約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : struct
        {
            return t;
        }

五、無參數構造函數約束 new()

/// new()約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : new()
        {
            return t;
        }

泛型約束也能夠同時約束多個,例如:

/// <summary>
        ///  泛型:不一樣的參數類型都能進來;任何類型都能過來,你知道我是誰?
        /// 沒有約束,也就沒有自由
        ///  泛型約束--基類約束(不能是sealed):
        /// 1 可使用基類的一切屬性方法---權利
        /// 2  強制保證T必定是People或者People的子類---義務
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter)
        where T : People, ISports, IWork, new()
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
            tParameter.Pingpang();
            tParameter.Work();
        }

注意:有多個泛型約束時,new()約束必定是在最後。

泛型的協變和逆變

public class Animal
    {
        public int Id { get; set; }
    }

    public class Cat : Animal
    {
        public string Name { get; set; }
    }
static void Main(string[] args)
 {
    #region 協變和逆變

    // 直接聲明Animal類
    Animal animal = new Animal();
    // 直接聲明Cat類
    Cat cat = new Cat();
    // 聲明子類對象指向父類
    Animal animal2 = new Cat();
    // 聲明Animal類的集合
    List<Animal> listAnimal = new List<Animal>();
    // 聲明Cat類的集合
    List<Cat> listCat = new List<Cat>();

    #endregion
 }

那麼問題來了:下面的一句代碼是否是正確的呢?

1 List<Animal> list = new List<Cat>();

可能有人會認爲是正確的:由於一隻Cat屬於Animal,那麼一羣Cat也應該屬於Animal啊。可是實際上這樣聲明是錯誤的:由於List 和List 之間沒有父子關係。

image-2020053023015097

這時就能夠用到協變和逆變了。

1 // 協變
2 IEnumerable<Animal> List1 = new List<Animal>();
3 IEnumerable<Animal> List2 = new List<Cat>();

F12查看定義:

能夠看到,在泛型接口的T前面有一個out關鍵字修飾,並且T只能是返回值類型,不能做爲參數類型,這就是協變。使用了協變之後,左邊聲明的是基類,右邊能夠聲明基類或者基類的子類。

協變除了能夠用在接口上面,也能夠用在委託上面:

Func<Animal> func = new Func<Cat>(() => null);

除了使用.NET框架定義好的覺得,咱們還能夠自定義協變,例如:

/// <summary>
    /// out 協變 只能是返回結果
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListOut<out T>
    {
        T Get();
    }

    public class CustomerListOut<T> : ICustomerListOut<T>
    {
        public T Get()
        {
            return default(T);
        }
    }

使用自定義的協變:

// 使用自定義協變
 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();

在來看看逆變。

在泛型接口的T前面有一個In關鍵字修飾,並且T只能方法參數,不能做爲返回值類型,這就是逆變。請看下面的自定義逆變:

/// <summary>
    /// 逆變 只能是方法參數
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListIn<in T>
    {
        void Show(T t);
    }

    public class CustomerListIn<T> : ICustomerListIn<T>
    {
        public void Show(T t)
        {
        }
    }

使用自定義逆變:

// 使用自定義逆變
 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>();
 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();

協變和逆變也能夠同時使用,看看下面的例子:

/// <summary>
    /// inT 逆變
    /// outT 協變
    /// </summary>
    /// <typeparam name="inT"></typeparam>
    /// <typeparam name="outT"></typeparam>
    public interface IMyList<in inT, out outT>
    {
        void Show(inT t);
        outT Get();
        outT Do(inT t);
    }

    public class MyList<T1, T2> : IMyList<T1, T2>
    {

        public void Show(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
        }

        public T2 Get()
        {
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }

        public T2 Do(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }
    }

使用:

IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//協變
 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆變
 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆變+協變

有關可變性的注意事項

  • 變化只適用於引用類型,由於不能直接從值類型派生其餘類型
  • 顯示變化使用in和out關鍵字只適用於委託和接口,不適用於類、結構和方法
  • 不包括in和out關鍵字的委託和接口類型參數叫作不變

泛型緩存

在前面咱們學習過,類中的靜態類型不管實例化多少次,在內存中只會有一個。靜態構造函數只會執行一次。在泛型類中,T類型不一樣,每一個不一樣的T類型,都會產生一個不一樣的副本,因此會產生不一樣的靜態屬性、不一樣的靜態構造函數,請看下面的例子:

public class GenericCache<T>
{
    static GenericCache()
    {
        Console.WriteLine("This is GenericCache 靜態構造函數");
        _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
    }

    private static string _TypeTime = "";

    public static string GetCache()
    {
        return _TypeTime;
    }
}
public class GenericCacheTest
{
    public static void Show()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(GenericCache<int>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<long>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<DateTime>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<string>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
            Thread.Sleep(10);
        }
    }
}

Main()方法裏面調用:

static void Main(string[] args)
 {
    #region 泛型緩存
	GenericCacheTest.Show();
    #endregion
 }

結果:

20200530232600809

從上面的截圖中能夠看出,泛型會爲不一樣的類型都建立一個副本,因此靜態構造函數會執行5次。 並且每次靜態屬性的值都是同樣的。利用泛型的這一特性,能夠實現緩存。

注意:只能爲不一樣的類型緩存一次。泛型緩存比字典緩存效率高。泛型緩存不能主動釋放。

注意

1.泛型代碼中的 default 關鍵字

在泛型類和泛型方法中會出現的一個問題是,如何把缺省值賦給參數化類型,此時沒法預先知道如下兩點:

  • T將是值類型仍是引用類型

  • 若是T是值類型,那麼T將是數值仍是結構

對於一個參數化類型T的變量t,僅當T是引用類型時,t = null語句纔是合法的; t = 0只對數值的有效,而對結構則不行。這個問題的解決辦法是用default關鍵字,它對引用類型返回空,對值類型的數值型返回零。而對於結構,它將返回結構每一個成員,並根據成員是值類型仍是引用類型,返回零或空。下面GenericList 類的例子顯示瞭如何使用default關鍵字。

static void Main(string[] args)
    {
        #region 泛型代碼默認關鍵字default
        // 使用非空的整數列表進行測試.
        GenericList<int> gll = new GenericList<int>();
        gll.AddNode(5);
        gll.AddNode(4);
        gll.AddNode(3);
        int intVal = gll.GetLast();
        // 下面一行顯示5.
        Console.WriteLine(intVal);

        // 用一個空的整數列表進行測試.
        GenericList<int> gll2 = new GenericList<int>();
        intVal = gll2.GetLast();
        // 下面一行顯示0.
        Console.WriteLine(intVal);

        // 使用非空字符串列表進行測試.
        GenericList<string> gll3 = new GenericList<string>();
        gll3.AddNode("five");
        gll3.AddNode("four");
        string sVal = gll3.GetLast();
        // 下面一行顯示five.
        Console.WriteLine(sVal);

        // 使用一個空字符串列表進行測試.
        GenericList<string> gll4 = new GenericList<string>();
        sVal = gll4.GetLast();
        // 下面一行顯示一條空白行.
        Console.WriteLine(sVal);
        #endregion
        Console.ReadKey();
    }
public class GenericList<T>
    {
        private class Node
        {
            // 每一個節點都有一個指向列表中的下一個節點的引用.
            public Node Next;
            // 每一個節點都有一個T類型的值.
            public T Data;
        }

        // 這個列表最初是空的.
        private Node head = null;

        // 在列表開始的時候添加一個節點,用t做爲它的數據值.
        public void AddNode(T t)
        {
            Node newNode = new Node();
            newNode.Next = head;
            newNode.Data = t;
            head = newNode;
        }

        // 下面的方法返回存儲在最後一個節點中的數據值列表. 若是列表是空的, 返回類型T的默認值.
        public T GetLast()
        {
            // 臨時變量的值做爲方法的值返回. 
            // 下面的聲明初始化了臨時的溫度 
            // 類型T的默認值. 若是該列表爲空返回默認值.
            T temp = default(T);

            Node current = head;
            while (current != null)
            {
                temp = current.Data;
                current = current.Next;
            }
            return temp;
        }
    }

2.泛型集合

一般狀況下,建議您使用泛型集合,由於這樣能夠得到類型安全的直接優勢而不須要從基集合類型派生並實現類型特定的成員。下面的泛型類型對應於現有的集合類型:

一、List 是對應於 ArrayList 的泛型類。
二、Dictionary 是對應於 Hashtable 的泛型類。
三、Collection 是對應於 CollectionBase 的泛型類。
四、ReadOnlyCollection 是對應於 ReadOnlyCollectionBase 的泛型類。
五、QueueStackSortedList 泛型類分別對應於與其同名的非泛型類。
六、LinkedList 是一個通用連接列表,它提供運算複雜度爲 O(1) 的插入和移除操做。
七、SortedDictionary 是一個排序的字典,其插入和檢索操做的運算複雜度爲 O(log n),這使得它成爲 SortedList 的十分有用的替代類型。
八、KeyedCollection 是介於列表和字典之間的混合類型,它提供了一種存儲包含本身鍵的對象的方法。

總結

  1. 做爲一個開發人員,當咱們程序代碼有相同的邏輯,有多是方法、接口、類或者委託,只是某些參數類型不一樣,咱們但願代碼能夠通用、複用,甚至是說爲了偷懶,也能夠說是在不肯定類型的狀況下,就應該考慮用泛型的思惟去實現。
  2. 在非泛型編程中,雖然全部的東西均可以做爲Object傳遞,可是在傳遞的過程當中免不了要進行類型轉換。而類型轉換在運行時是不安全的。使用泛型編程將能夠減小沒必要要的類型轉換,從而提升安全性。不只是值類型,引用類型也存在這樣的問題,所以有必要的儘可能的去使用泛型集合。
  3. 在非泛型編程中,將簡單類型做爲Object傳遞時會引發裝箱和拆箱的操做,這兩個過程都是具備很大開銷的。使用泛型編程就沒必要進行裝箱和拆箱操做了。

參考 文檔 《C#圖解教程》

注:搜索關注公衆號【DotNet技術谷】--回覆【C#圖解】,可獲取 C#圖解教程文件

相關文章
相關標籤/搜索