3

在阅读了一本关于 LINQ 的书后,我正在考虑重新编写一个我用 c# 编写的映射器类以使用 LINQ。我想知道是否有人可以帮我一把。注意:这有点令人困惑,但 User 对象是本地用户,而 user(小写)是从 Facebook XSD 生成的对象。

原始映射器

public class FacebookMapper : IMapper
{
    public IEnumerable<User> MapFrom(IEnumerable<User> users)
    {
      var facebookUsers = GetFacebookUsers(users);
      return MergeUsers(users, facebookUsers);
    }

    public Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
      var uids = (from u in users
        where u.FacebookUid != null
        select u.FacebookUid.Value).ToList();

      // return facebook users for uids using WCF
    }

    public IEnumerable<User> MergeUsers(IEnumerable<User> users, Facebook.user[] facebookUsers)
    {
      foreach(var u in users)
      {
        var fbUser = facebookUsers.FirstOrDefault(f => f.uid == u.FacebookUid);
        if (fbUser != null)
          u.FacebookAvatar = fbUser.pic_sqare;
      }
      return users;
    }
}

我的前两次尝试碰壁了

尝试 1

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
  // didn't have a way to check if u.FacebookUid == null
  return from u in users
    join f in GetFacebookUsers(users) on u.FacebookUid equals f.uid
    select AppendAvatar(u, f);
}

public void AppendAvatar(User u, Facebook.user f)
{
  if (f == null)
    return u;
  u.FacebookAvatar = f.pic_square;
  return u;
}

尝试 2

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
  // had to get the user from the facebook service for each single user,
  // would rather use a single http request.
  return from u in users
    let f = GetFacebookUser(user.FacebookUid)
    select AppendAvatar(u, f);
}
4

2 回答 2

9

好的,目前尚不清楚其中的确切内容IMapper,但我会提出一些建议,其中一些可能由于其他限制而不可行。我已经按照我的想法写了这个 - 我认为这有助于看到思路在行动,因为这会让你下次更容易做同样的事情。(当然,假设您喜欢我的解决方案:)

LINQ 本质上是函数式的。这意味着理想情况下,查询不应该有副作用。例如,我期望一个带有以下签名的方法:

public IEnumerable<User> MapFrom(IEnumerable<User> users)

返回具有额外信息的新用户对象序列,而不是改变现有用户。您当前添加的唯一信息是头像,因此我将添加一个方法,User如下所示:

public User WithAvatar(Image avatar)
{
    // Whatever you need to create a clone of this user
    User clone = new User(this.Name, this.Age, etc);
    clone.FacebookAvatar = avatar;
    return clone;
}

您甚至可能希望User完全不可变 - 围绕它有各种策略,例如构建器模式。问我是否需要更多详细信息。无论如何,最主要的是我们创建了一个新用户,它是旧用户的副本,但具有指定的头像。

第一次尝试:内连接

现在回到你的映射器……你目前有三个公共方法,但我只有第一个需要公共,API 的其余部分实际上不需要公开 Facebook 用户。看起来您的GetFacebookUsers方法基本上没问题,尽管我可能会根据空格排列查询。

因此,给定一系列本地用户和一组 Facebook 用户,我们只剩下实际的映射位了。直接的“加入”子句是有问题的,因为它不会产生没有匹配 Facebook 用户的本地用户。相反,我们需要某种方式来对待非 Facebook 用户,就好像他们是没有头像的 Facebook 用户一样。本质上这是空对象模式。

我们可以通过创建一个拥有空 uid 的 Facebook 用户来做到这一点(假设对象模型允许这样做):

// Adjust for however the user should actually be constructed.
private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);

但是,我们实际上想要这些用户的序列,因为这就是Enumerable.Concat使用:

private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
    Enumerable.Repeat(new FacebookUser(null), 1);

现在我们可以简单地将这个虚拟条目“添加”到我们的真实条目中,并进行正常的内部连接。请注意,这假设Facebook 用户的查找总是会找到任何“真实”Facebook UID 的用户。如果不是这种情况,我们需要重新审视这一点,而不是使用内部连接。

我们在最后包含“null”用户,然后使用以下方法进行连接和项目WithAvatar

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
    return from user in users
           join facebookUser in facebookUsers on
                user.FacebookUid equals facebookUser.uid
           select user.WithAvatar(facebookUser.Avatar);
}

所以全班将是:

public sealed class FacebookMapper : IMapper
{
    private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
        Enumerable.Repeat(new FacebookUser(null), 1);

    public IEnumerable<User> MapFrom(IEnumerable<User> users)
    {
        var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
        return from user in users
               join facebookUser in facebookUsers on
                    user.FacebookUid equals facebookUser.uid
               select user.WithAvatar(facebookUser.pic_square);
    }

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).ToList();

        // return facebook users for uids using WCF
    }
}

这里有几点:

  • 如前所述,如果用户的 Facebook UID 可能无法作为有效用户获取,则内部连接会出现问题。
  • 同样,如果我们有重复的 Facebook 用户,我们也会遇到问题——每个本地用户最终会出现两次!
  • 这将替换(删除)非 Facebook 用户的头像。

第二种方法:组加入

让我们看看我们是否可以解决这些问题。我假设如果我们为单个 Facebook UID 获取了多个Facebook 用户,那么我们从他们中的哪一个获取头像并不重要——它们应该是相同的。

我们需要的是组加入,这样对于每个本地用户,我们都会获得一系列匹配的 Facebook 用户。然后我们将使用它DefaultIfEmpty来让生活更轻松。

我们可以保持WithAvatar以前的样子——但这次我们只有在有 Facebook 用户可以从中获取头像时才调用它。C# 查询表达式中的组连接由 表示join ... into。这个查询相当长,但不是太吓人,老实说!

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users);
    return from user in users
           join facebookUser in facebookUsers on
                user.FacebookUid equals facebookUser.uid
                into matchingUsers
           let firstMatch = matchingUsers.DefaultIfEmpty().First()
           select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
}

这是查询表达式,但带有注释:

// "Source" sequence is just our local users
from user in users
// Perform a group join - the "matchingUsers" range variable will
// now be a sequence of FacebookUsers with the right UID. This could be empty.
join facebookUser in facebookUsers on
     user.FacebookUid equals facebookUser.uid
     into matchingUsers
// Convert an empty sequence into a single null entry, and then take the first
// element - i.e. the first matching FacebookUser or null
let firstMatch = matchingUsers.DefaultIfEmpty().First()
// If we've not got a match, return the original user.
// Otherwise return a new copy with the appropriate avatar
select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);

非 LINQ 解决方案

另一种选择是仅非常轻微地使用 LINQ。例如:

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users);
    var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);

    foreach (var user in users)
    {
        FacebookUser fb;
        if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
        {
            yield return user.WithAvatar(fb.pic_square);
        }
        else
        {
            yield return user;
        }
    }
}

这使用迭代器块而不是 LINQ 查询表达式。ToDictionary如果两次收到相同的密钥,将引发异常 - 解决此问题的一个选项是更改GetFacebookUsers以确保它只查找不同的 ID:

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).Distinct().ToList();

        // return facebook users for uids using WCF
    }

当然,这假设 Web 服务可以正常工作 - 但如果不能正常工作,您可能还是想抛出异常 :)

结论

从三者中挑选。组加入可能最难理解,但表现最好。迭代器块解决方案可能是最简单的,并且在GetFacebookUsers修改时应该表现良好。

不过,制作User不可变几乎肯定是一个积极的步骤。

所有这些解决方案的一个很好的副产品是用户按照他们进入的顺序出现。这对你来说可能并不重要,但它可能是一个很好的属性。

希望这会有所帮助 - 这是一个有趣的问题:)

编辑:突变是要走的路吗?

在您的评论中看到本地用户类型实际上是实体框架中的实体类型,采取这种行动可能不合适。让它不可变几乎是不可能的,我怀疑该类型的大多数用途都会发生突变。

如果是这种情况,可能值得更改您的界面以使其更清晰。您可能希望同时更改签名和名称,而不是返回一个IEnumerable<User>(这意味着 - 在某种程度上 - 投影),给您留下如下内容:

public sealed class FacebookMerger : IUserMerger
{
    public void MergeInformation(IEnumerable<User> users)
    {
        var facebookUsers = GetFacebookUsers(users);
        var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);

        foreach (var user in users)
        {
            FacebookUser fb;
            if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
            {
                user.Avatar = fb.pic_square;
            }
        }
    }

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).Distinct().ToList();

        // return facebook users for uids using WCF
    }
}

同样,这不再是一个特别的“LINQ-y”解决方案(在主要操作中) - 但这是合理的,因为您并不是真正的“查询”;你在“更新”。

于 2009-04-01T19:28:06.250 回答
5

我会倾向于写这样的东西:

public class FacebookMapper : IMapper
{
    public IEnumerable<User> MapFacebookAvatars(IEnumerable<User> users)
    {
        var usersByID =
            users.Where(u => u.FacebookUid.HasValue)
                 .ToDictionary(u => u.FacebookUid.Value);

        var facebookUsersByID =
            GetFacebookUsers(usersByID.Keys).ToDictionary(f => f.uid);

        foreach(var id in usersByID.Keys.Intersect(facebookUsersByID.Keys))
            usersByID[id].FacebookAvatar = facebookUsersByID[id].pic_sqare;

        return users;
    }

    public Facebook.user[] GetFacebookUsers(IEnumerable<int> uids)
    {
       // return facebook users for uids using WCF
    }
}

但是,我不会声称这对您所拥有的东西有很大的改进(除非用户或 facebook 用户集合非常大,在这种情况下,您最终可能会出现明显的性能差异。)

(我建议不要使用Selectlikeforeach循环对集合的元素执行实际的变异操作,就像您在重构尝试中所做的那样。您可以这样做,但人们会对您的代码感到惊讶,您会必须始终牢记惰性评估。)

于 2009-03-28T21:59:26.573 回答