C#匿名函数的使用奥义

尝试从缓存中获取数据,如果数据存在则返回,否则从数据源中获取数据,放入缓存,然后返回。
您是否熟悉上面这段逻辑说明?如果您的应用中大量使用了缓存,则上面这段逻辑很可能会出现许多次。例如:
CacheManager cacheManager = new CacheManager();


public List<User> GetFriends(int userId)
{

string cacheKey = "friends_of_user_" + userId;



object objResult = cacheManager.Get(cacheKey);

if (objResult != null) return (List<User>)objResult;



List<User> result = new UserService().GetFriends(userId);

cacheManager.Set(cacheKey, result);



return result;
}
这段逻辑似乎比较简单,不过在实际应用中,从数据源中获取数据可能不是简单地调用一个方法,而是需要多个类之间的协作,事务控制等等,而缓存的读写可能 也会比上面的示例来的复杂。因此,一个可读性高的做法是提供三个独立的方法(读取缓存,读取数据源,写入缓存),使得一个拥有缓存的方法只需要简单地实现 上面所提到的读写逻辑即可。
正如文章开头所说,如果您的应用中大量使用了缓存,则上面这段逻辑很可能会出现许多次。在一定程度上这种重复也是多余的,违背了DRY原则。因此我们设法提供一个基类,把这段缓存读写逻辑封装起来:
public abstract class CacheReader<T>
{

///
<summary>从缓存中获取数据</summary>

///
<param name="data">从缓存中取得的数据</param>

///
<returns>从缓存中成功取得数据则返回true,反之则false</returns>

public abstract bool GetFromCache(out T data);



///
<summary>从数据源获取数据</summary>

///
<returns>从数据源取得的对象</returns>

public abstract T ReadFromSource();



///
<summary>将数据写入缓存</summary>

///
<param name="data">将要写入缓存的数据</param>

public abstract void SetToCache(T data);



public T Read()

{

T data;

if (this.GetFromCache(out data)) return data;



data = this.ReadFromSource();

this.SetToCache(data);



return data;

}
}
于是我们将这段缓存读写逻辑集中到了CacheReader类的Read方法中。而对于每个缓存读写操作,我们只要实现一个CacheReader类的子类,提供三个抽象方法的具体实现即可。如下:
private class GetFriendCacheReader : CacheReader<List<User>>
{

private int m_userId;

private string m_cacheKey;

private CacheManager m_cacheManager;



public GetFriendCacheReader(int userId, CacheManager cacheManager)

{

this.m_userId = userId;

this.m_cacheKey = "friends_of_user_" + userId;

this.m_cacheManager = cacheManager;

}



public override bool GetFromCache(out List<User> data)

{

object objData = this.m_cacheManager.Get(this.m_cacheKey);

if (objData == null)

{

data = null;

return false;

}



data = (List<User>)objData;

return true;

}



public override List<User> ReadFromSource()

{

return new UserService().GetFriends(this.m_userId);

}



public override void SetToCache(List<User> data)

{

this.m_cacheManager.Set(this.m_cacheKey, data);

}
}
于是我们的GetFriends方法就可以修改成如下模样:
public List<User> GetFriends(int userId)
{

return new GetFriendCacheReader(userId, cacheManager).Read();
}
典型的“模板方法(Template Method)”模式的应用,真是……优雅?这是明显的“矫枉过正”!一个GetFriends方法需要开发一个类,而应用中少说也该有几十上百个这样的 方法吧?于是乎,我们吭哧吭哧地开发了几十上百个CacheReader的子类,每个子类需要把所有用到的对象和数据封装进去,并且实现三个抽象方法—— OMG,真可谓“OOP”到了极致!
还好,我们可以使用匿名方法。为此,我们写一个Helper方法:
public static class CacheHelper
{

public delegate bool CacheGetter<TData>(out TData data);



public static TData Get<TData>(

CacheGetter<TData> cacheGetter,

Func<TData> sourceGetter,

Action<TData> cacheSetter)

{

TData data;

if (cacheGetter(out data))

{

return data;

}



data = sourceGetter();

cacheSetter(data);



return data;

}
}
委托是个好东西,可以作为方法的参数使用,而匿名方法的存在让这种方式变得尤其有用。例如,我们现在就可以:
public List<User> GetFriends(int userId)
{

string cacheKey = "friends_of_user_" + userId;



return CacheHelper.Get(

delegate(out List<User> data) // cache getter

{

object objData = cacheManager.Get(cacheKey);

data = (objData == null) ? null : (List<User>)objData;



return objData != null;

},

() => // source getter

{

return new UserService().GetFriends(userId);

},

(data) => // cache setter

{

cacheManager.Set(cacheKey, data);

});


}
看上去是不是有点古怪?其实习惯了就好。这种做法有好处还不少:

  • 可读性好:操作的逻辑被分割在不同block中。
  • 编程方便:能够直接使用方法的参数和外部对象,不会有封装的麻烦。
  • 调试方便:设置断点之后可以轻松看出“从缓存中读取”、“从数据源读取”和“写入缓存”的过程。

在出现匿名方法之后,这种将委托作为参数传入方法的做法其实已经非常普遍了。例如在微软推出的并行库中就能使用同样的调用方式:
void ParallelCalculate()
{

double result = 0;

object syncObj = new object();



List<int> list = GetIntList();



Parallel.For<double>(

0,

list.Count,

() => 0,

(index, ps) =>

{

int value = list[index];

for (int n = 0; n < 100; n++)

{

ps.ThreadLocalState += Math.Sqrt(value) * Math.Sin(value);

}

},

(threadResult) =>

{

lock (syncObj)

{

result += threadResult;

}

});



Console.WriteLine("result = " + result);
}
您接受这种做法了吗?
您善于使用匿名函数吗?

共有0个回答