dotnet 职业技术学院

博客

dotnet 职业技术学院

WPF 的 Application.Current.Dispatcher 中,Dispatcher 属性一定不会为 null

dotnet 职业技术学院 发布于 2019-08-27

在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx 这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException,于是就有三位小伙伴告诉我说 CurrentDispatcher 属性都可能为 null

然而实际上这里只可能 Currentnull 而此上下文的 Dispatcher 是绝对不会为 null 的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 thisnull 呢……)


由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。

Application.Dispatcher 实例属性

Application.Dispatcher 实例属性来自于 DispatcherObject

源代码

为了分析此属性是否可能为 null,我现在将 DispatcherObject 的全部代码贴在下面:

using System;
using System.Windows;
using System.Threading;
using MS.Internal.WindowsBase;               // FriendAccessAllowed
 
namespace System.Windows.Threading
{
    /// <summary>
    ///     A DispatcherObject is an object associated with a
    ///     <see cref="Dispatcher"/>.  A DispatcherObject instance should
    ///     only be access by the dispatcher's thread.
    /// </summary>
    /// <remarks>
    ///     Subclasses of <see cref="DispatcherObject"/> should enforce thread
    ///     safety by calling <see cref="VerifyAccess"/> on all their public
    ///     methods to ensure the calling thread is the appropriate thread.
    ///     <para/>
    ///     DispatcherObject cannot be independently instantiated; that is,
    ///     all constructors are protected.
    /// </remarks>
    public abstract class DispatcherObject
    {
        /// <summary>
        ///     Returns the <see cref="Dispatcher"/> that this
        ///     <see cref="DispatcherObject"/> is associated with.
        /// </summary>
        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
        public Dispatcher Dispatcher
        {
            get
            {
                // This property is free-threaded.
 
                return _dispatcher;
            }
        }
 
        // This method allows certain derived classes to break the dispatcher affinity
        // of our objects.
        [FriendAccessAllowed] // Built into Base, also used by Framework.
        internal void DetachFromDispatcher()
        {
            _dispatcher = null;
        }
 
        // Make this object a "sentinel" - it can be used in equality tests, but should
        // not be used in any other way.  To enforce this and catch bugs, use a special
        // sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
        // fail;  this will catch most accidental uses of the sentinel.
        [FriendAccessAllowed] // Built into Base, also used by Framework.
        internal void MakeSentinel()
        {
            _dispatcher = EnsureSentinelDispatcher();
        }
 
        private static Dispatcher EnsureSentinelDispatcher()
        {
            if (_sentinelDispatcher == null)
            {
                // lazy creation - the first thread reaching here creates the sentinel
                // dispatcher, all other threads use it.
                Dispatcher sentinelDispatcher = new Dispatcher(isSentinel:true);
                Interlocked.CompareExchange<Dispatcher>(ref _sentinelDispatcher, sentinelDispatcher, null);
            }
 
            return _sentinelDispatcher;
        }
 
        /// <summary>
        ///     Checks that the calling thread has access to this object.
        /// </summary>
        /// <remarks>
        ///     Only the dispatcher thread may access DispatcherObjects.
        ///     <p/>
        ///     This method is public so that any thread can probe to
        ///     see if it has access to the DispatcherObject.
        /// </remarks>
        /// <returns>
        ///     True if the calling thread has access to this object.
        /// </returns>
        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
        public bool CheckAccess()
        {
            // This method is free-threaded.
 
            bool accessAllowed = true;
            Dispatcher dispatcher = _dispatcher;
 
            // Note: a DispatcherObject that is not associated with a
            // dispatcher is considered to be free-threaded.
            if(dispatcher != null)
            {
                accessAllowed = dispatcher.CheckAccess();
            }
 
            return accessAllowed;
        }
 
        /// <summary>
        ///     Verifies that the calling thread has access to this object.
        /// </summary>
        /// <remarks>
        ///     Only the dispatcher thread may access DispatcherObjects.
        ///     <p/>
        ///     This method is public so that derived classes can probe to
        ///     see if the calling thread has access to itself.
        /// </remarks>
        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
        public void VerifyAccess()
        {
            // This method is free-threaded.
 
            Dispatcher dispatcher = _dispatcher;
 
            // Note: a DispatcherObject that is not associated with a
            // dispatcher is considered to be free-threaded.
            if(dispatcher != null)
            {
                dispatcher.VerifyAccess();
            }
        }
 
        /// <summary>
        ///     Instantiate this object associated with the current Dispatcher.
        /// </summary>
        protected DispatcherObject()
        {
            _dispatcher = Dispatcher.CurrentDispatcher;
        }
 
        private Dispatcher _dispatcher;
        private static Dispatcher _sentinelDispatcher;
    }
}

代码来自:DispatcherObject.cs

Dispatcher 属性仅仅是在获取 _dispatcher 字段的值,因此我们只需要看 _dispatcher 字段的赋值时机,以及所有给 _dispatcher 赋值的代码。

由于 _dispatcher 字段是私有字段,所以仅需调查这个类本身即可找到所有的赋值时机。(反射等非常规手段需要排除在外,因为这意味着开发者是逗比——自己闯的祸不能怪 WPF 框架。)

赋值时机

先来看看 dispatcher 字段的赋值时机。

DispatcherObject 仅有一个构造函数,而这个构造函数中就已经给 _dispatcher 赋值了,因此其所有的子类的初始化之前,_dispatcher 就会被赋值。

protected DispatcherObject()
{
    _dispatcher = Dispatcher.CurrentDispatcher;
}

那么所赋的值是否可能为 null 呢,这就要看 Dispatcher.CurrentDispatcher 是否可能返回一个 null 了。

以下是 Dispatcher.CurrentDispatcher 的属性获取代码:

public static Dispatcher CurrentDispatcher
{
    get
    {
        // Find the dispatcher for this thread.
        Dispatcher currentDispatcher = FromThread(Thread.CurrentThread);;

        // Auto-create the dispatcher if there is no dispatcher for
        // this thread (if we are allowed to).
        if(currentDispatcher == null)
        {
            currentDispatcher = new Dispatcher();
        }

        return currentDispatcher;
    }
}

可以看到,无论前面的方法得到的值是否是 null,后面都会再给 currentDispatcher 局部变量赋值一个新创建的实例的。因此,此属性是绝对不会返回 null 的。

由此可知,DispatcherObject 自构造起便拥有一个不为 nullDispatcher 属性,其所有子类在初始化之前便会得到不为 nullDispatcher 属性。

后续赋值

现在我们来看看在初始化完成之后,后面是否有可能将 _dispatcher 赋值为 null。

_dispatcher 字段的赋值代码仅有两个:

// This method allows certain derived classes to break the dispatcher affinity
// of our objects.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void DetachFromDispatcher()
{
    _dispatcher = null;
}

// Make this object a "sentinel" - it can be used in equality tests, but should
// not be used in any other way.  To enforce this and catch bugs, use a special
// sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
// fail;  this will catch most accidental uses of the sentinel.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void MakeSentinel()
{
    _dispatcher = EnsureSentinelDispatcher();
}

第一个 DetachFromDispatcher 很好理解,让 DispatcherObjectDispatcher 无关。在整个 WPF 的代码中,使用此方法的仅有以下 6 处:

  • Freezable.Freeze 实例方法
  • BeginStoryboard.Seal 实例方法
  • Style.Seal 实例方法
  • TriggerBase.Seal 实例方法
  • StyleHelperSealTemplate 静态方法中对 FrameworkTemplate 类型的实例调用此方法
  • ResourceDictionary 在构造函数中为 DispatcherObject 类型的 DummyInheritanceContext 属性调用此方法

Application 类型不是以上任何一个类型的子类(Application 类的直接基类是 DispatcherObject),因此 Application 类中的 Dispatcher 属性不可能因为 DetachFromDispatcher 方法的调用而被赋值为 null

接下来看看 MakeSentinel 方法,此方法的作用不如上面方法那样直观,实际上它的作用仅仅为了验证某个方法调用时所在的线程是否是符合预期的(给 VerifyAccessCheckAccess 使用)。

使用此方法的仅有 1 处:

  • ItemsControl 所用的 ItemInfo 类的静态构造函数
internal static readonly DependencyObject SentinelContainer = new DependencyObject();
internal static readonly DependencyObject UnresolvedContainer = new DependencyObject();
internal static readonly DependencyObject KeyContainer = new DependencyObject();
internal static readonly DependencyObject RemovedContainer = new DependencyObject();

static ItemInfo()
{
    // mark the special DOs as sentinels.  This helps catch bugs involving
    // using them accidentally for anything besides equality comparison.
    SentinelContainer.MakeSentinel();
    UnresolvedContainer.MakeSentinel();
    KeyContainer.MakeSentinel();
    RemovedContainer.MakeSentinel();
}

所有这些使用都与 Application 无关。

结论

总结以上所有的分析:

  1. Application 类型的实例在初始化之前,Dispatcher 属性就已经被赋值且不为 null
  2. 所有可能改变 _dispatcher 属性的常规方法均与 Application 类型无关;

因此,所有常规手段均不会让 Application 类的 Dispatcher 属性拿到 null 值。如果你还说拿到了 null,那就检查是否有逗比程序员通过反射或其他手段将 _dispatcher 字段改为了 null 吧……

Application.Current 静态属性

关于 Application.Current 是否可能为 null 的分析,由于比较长,请参见我的另一篇博客:


参考资料

使用 SetParent 跨进程设置父子窗口时的一些问题(小心卡死)

dotnet 职业技术学院 发布于 2019-08-27

在微软的官方文档中,说 SetParent 可以在进程内设置,也可以跨进程设置。当使用跨进程设置窗口的父子关系时,你需要注意本文提到的一些问题,避免踩坑。


跨进程设置 SetParent

关于 SetParent 函数设置窗口父子关系的文档可以看这个:

在这篇文章的 DPI 感知一段中明确写明了在进程内以及跨进程设置父子关系时的一些行为。虽然没有明确说明支持跨进程设置父子窗口,不过这段文字就几乎说明 Windows 系统对于跨进程设置窗口父子关系还是支持的。

但 Raymond Chen 在 Is it legal to have a cross-process parent/child or owner/owned window relationship? 一文中有另一段文字:

If I remember correctly, the documentation for Set­Parent used to contain a stern warning that it is not supported, but that remark does not appear to be present any more. I have a customer who is reparenting windows between processes, and their application is experiencing intermittent instability.
如果我没记错的话,SetParent 的文档曾经包含一个严厉的警告表明它不受支持,但现在这段备注似乎已经不存在了。我就遇到过一个客户跨进程设置窗口之间的父子关系,然后他们的应用程序间歇性不稳定。

这里表明了 Raymond Chen 对于跨进程设置父子窗口的一些担忧,但从文档趋势来看,还是支持的。只是这种担忧几乎说明跨进程设置 SetParent 存在一些坑。

那么本文就说说跨进程设置父子窗口的一些坑。

消息循环强制同步

消息循环

我们会感觉到 Windows 中某个窗口有响应(比如鼠标点击有反应),是因为这个窗口在处理 Windows 消息。窗口进行消息循环不断地处理消息使得各种各样的用户输入可以被处理,并正确地在界面上显示。

一个典型的消息循环大概像这样:

while(GetMessage(ref msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

对于显示了窗口的某个线程调用了 GetMessage 获取了消息,Windows 系统就会认为这个线程有响应。相反,如果长时间不调用 GetMessage,Windows 就会认为这个线程无响应。TranslateMessage 则是翻译一些消息(比如从按键消息翻译成字符消息)。真正处理 GetMessage 中的内容则是后面的调度消息 DispatchMessage,是这个函数的调用使得我们 UI 界面上的内容可以有可见的反映。

一般来说,每个创建了窗口的线程都有自己独立的消息循环,且不会互相影响。然而一旦这些窗口之间建立了父子关系之后就会变得麻烦起来。

强制同步

Windows 会让具有父子关系的所有窗口的消息循环强制同步。具体指的是,所有具有父子关系的窗口消息循环,其消息循环会串联成一个队列(这样才可以避免消息循环的并发)。

也就是说,如果你有 A、B、C、D 四个窗口,分属不同进程,A 是 B、C、D 窗口的父窗口,那么当 A 在处理消息的时候,B、C、D 的消息循环就会卡在 GetMessage 的调用。同样,无论是 B、C 还是 D 在处理消息的时候,其他窗口也会同样卡在 GetMessage 的调用。这样,所有进程的 UI 线程实际上会互相等待,所有通过消息循环执行的代码都不会同时执行。然而实际上 Windows GUI 应用程序的开发中基本上 UI 代码都是通过消息循环来执行的,所以这几乎等同于所有进程的 UI 线程强制同步成类似一个 UI 线程的效果了。

带来的副作用也就相当明显,任何一个进程卡了 UI,其他进程的 UI 将完全无响应。当然,不依赖消息循环的代码不会受此影响,比如 WPF 应用程序的动画和渲染。

如何解决

对于 SetParent 造成的这些问题,实际上没有官方的解决方案,你需要针对你不同的业务采用不同的解决办法。

正如 Raymond Chen 所说:

(It’s one of those “if you don’t already know what the consequences are, then you are not smart enough to do it correctly” things. You must first become the master of the rules before you can start breaking them.)
正如有些人说的“如果你不知道后果,那么你也不足以正确地完成某件事情”。在开始破坏规则之前,您必须先成为规则的主人。

你必须清楚跨进程设置父子窗口带来的各种副作用,然后针对性地给出解决方案:

  1. 比如所有窗口会强制串联成一个队列,那么可以考虑将暂时不显示的窗口断开父子关系;
  2. 比如设置窗口的位置大小等操作,必须考虑此窗口不是顶层窗口的问题,需要跨越进程到顶层窗口来操作;

参考资料

程序员可能会使用的各种命名规则

dotnet 职业技术学院 发布于 2019-08-19

古老的程序员们有时会纠结命名问题,而现在,程序员们的命名已经开创了数个流派。本文整理了程序员们命名会使用到的各种流派,当然一些编程语言会同时使用数个流派。


PascalCase

有很多个名字,除了 PascalCase 还有 UpperCamelCase, BumpyCase。

所有单词直接连接,连接的每个单词的首字母大写。

WelcomeToReadWalterlvBlog

camelCase

所有单词直接连接,连接的每个单词的首字母大写。

walterlvIsADoubi

snake_case

单词的所有字母小写,单词之间通过下划线 _ 连接起来。

welcome_to_read_walterlv_blog

kebab-case

单词的所有字母小写,单词之间通过连字符(hyphen,-)连接起来。

walterlv-is-a-doubi


参考资料

System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”

dotnet 职业技术学院 发布于 2019-08-14

当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“BuildWindowCore 无法返回寄宿的子窗口句柄。”。

这是很典型的 Win32 错误,本文介绍如何修复此错误。


我们在 MainWindow 中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。

WPF 中可以使用 HwndSource 来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost 的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。

问题

你有可能在调试嵌入窗口代码的时候遇到错误:

错误

System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”

英文是:

BuildWindowCore failed to return the hosted child window handle.

原因和解决办法

此异常的原因非常简单,是 HwndSourceBuildWindowCore 的返回值有问题。具体来说,就是子窗口的句柄返回了 0。

也就是下面这段代码中 return new HandleRef(this, IntPtr.Zero) 这句,第二个参数是 0。

protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
    const int WS_CHILD = 1073741824;
    const int WS_CLIPCHILDREN = 33554432;
    var parameters = new HwndSourceParameters("demo")
    {
        ParentWindow = hwndParent.Handle,
        WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
        TreatAncestorsAsNonClientArea = true,
    };
    var source = new HwndSource(parameters);
    source.RootVisual = new Button();
    return new HandleRef(this, _handle);
}

要解决,就需要传入正确的句柄值。当然上面的代码为了示例,故意传了一个不知道哪里的 _handle,实际上应该传入 source.Handle 才是正确的。

System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”

dotnet 职业技术学院 发布于 2019-08-14

当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“System.InvalidOperationException:“寄宿 HWND 必须是子窗口。””。

这是很典型的 Win32 错误,本文介绍如何修复此错误。


一个最简的嵌入其他窗口的例子

我们在 MainWindow 中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。

WPF 中可以使用 HwndSource 来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost 的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。

下面是最简单的一个例子,为了简单,没有跨进程传递 Win32 窗口句柄,而是直接创建出来。

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

namespace Walterlv.Demo.HwndWrapping
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Content = new HwndWrapper();
        }
    }

    public class HwndWrapper : HwndHost
    {
        private HwndSource _source;

        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            var parameters = new HwndSourceParameters("walterlv");
            _source = new HwndSource(parameters);
            // 这里的 ChildPage 是一个继承自 UseControl 的 WPF 控件,你可以自己创建自己的 WPF 控件。
            _source.RootVisual = new ChildPage();
            return new HandleRef(this, _source.Handle);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
            _source?.Dispose();
        }
    }
}

寄宿 HWND 必须是子窗口

当运行此代码的时候,会提示错误:

System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”

或者英文版:

System.InvalidOperationException:”Hosted HWND must be a child window.”

这是一个 Win32 错误,因为我们试图将一个普通的窗口嵌入到另一个窗口中,而实际上要完成嵌入需要子窗口才行。

那么如何设置一个 Win32 窗口为子窗口呢?使用 SetWindowLong 来设置 Win32 窗口的样式是可以的。不过我们因为使用了 HwndSource,所以可以通过 HwndSourceParameters 来更方便地设置窗口样式。

我们需要将 HwndSourceParameters 那一行改成这样:

++  const int WS_CHILD = 0x40000000;
--  var parameters = new HwndSourceParameters("walterlv");
++  var parameters = new HwndSourceParameters("walterlv")
++  {
++      ParentWindow = hwndParent.Handle,
++      WindowStyle = WS_CHILD,
++  };

最关键的是两点:

  1. 需要设置此窗口为子窗口,也就是设置 WindowStyleWS_CHILD
  2. 需要设置此窗口的父窗口,也就是设置 ParentWindowhwndParent.Handle(我们使用参数中传入的 hwndParent 作为父窗口)。

现在再运行,即可正常显示此嵌套窗口:

嵌套窗口

另外,WindowStyle 属性最好加上 WS_CLIPCHILDREN,详情请阅读:


参考资料

System.InvalidOperationException:“寄宿的 HWND 必须是指定父级的子窗口。”

dotnet 职业技术学院 发布于 2019-08-14

当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“寄宿的 HWND 必须是指定父级的子窗口。”。

这是很典型的 Win32 错误,本文介绍如何修复此错误。


我们在 MainWindow 中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。

WPF 中可以使用 HwndSource 来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost 的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。

问题

你有可能在调试嵌入窗口代码的时候遇到错误:

错误

System.InvalidOperationException:“寄宿的 HWND 必须是指定父级的子窗口。”

英文是:

Hosted HWND must be a child window of the specified parent.

原因和解决办法

出现此错误,是因为同一个子窗口被两次设置为同一个窗口的子窗口。

具体来说,就是 A 窗口使用 HwndHost 设置成了 B 的子窗口,随后 A 又通过一个新的 HwndHost 设置成了新子窗口。

要解决,则必须确保一个窗口只能使用 HwndHost 设置一次子窗口。

通过 mklink 收集本地文件系统的所有 NuGet 包输出目录来快速调试公共组件代码

dotnet 职业技术学院 发布于 2019-08-04

我们做的公共库可能通过 nuget.org 发布,也可能是自己搭建 NuGet 服务器。但是,如果某个包正在开发中,需要快速验证其是否解决掉一些诡异的 bug 的话,除了单元测试这种间接的测试方法,还可以在本地安装未发布的 NuGet 包的方法来快速调试。

本文介绍如何本地打包发布 NuGet 包,然后通过 mklink 收集所有的本地包达到快速调试的目的。


将本地文件夹作为 NuGet 源

我有另一篇博客介绍如何将本地文件夹设置称为 NuGet 包源:

在 Visual Studio 中打开 工具 -> 选项 -> NuGet 包管理器 -> 包源 可以直接将一个本地文件夹设置称为 NuGet 包源。

管理包源

其他设置方法可以去那篇博客当中阅读。

如下图,是我通过 mklink 将散落在各处的 NuGet 包的调试输出目录收集了起来:

通过 mklink 收集的 NuGet 包源

比如,点开其中的 Walterlv.Packages 可以看到 Walterlv.Packages 仓库中输出的 NuGet 包:

其中的一个 NuGet 输出文件夹

由于我的每一个文件夹都是指向的 Visual Studio 编译后的输出目录,所以,只需要使用 Visual Studio 重新编译一下项目,文件夹中的 NuGet 包即会更新。

于是,这相当于我在一个文件夹中,包含了我整个计算机上所有库项目的 NuGet 包,只需要将这个文件夹设置称为 NuGet 包源,即可直接调试本地任何一个公共组件库打出来的 NuGet 包。

设置源并体验快速调试

如下图,是我将那个收集所有 NuGet 文件夹的目录设置成为了 NuGet 源:

设置的本地 NuGet 源

于是,我可以在 Visual Studio 的包管理器中看到所有还没有发布的,依然处于调试状态的各种库:

各种处于调试状态的各种库

基于此,我们可以在包还没有编写完的时候调试,验证速度非常快。

使用 C# 中的 dynamic 关键字调用类型方法时可能遇到的各种问题

dotnet 职业技术学院 发布于 2019-08-04

你可以使用 dynamic 来定义一个变量或者字段,随后你可以像弱类型语言一样调用这个实例的各种方法,就像你一开始就知道这个类型的所有属性和方法一样。

但是,使用不当又会遇到各种问题,本文收集使用过程中可能会遇到的各种问题,帮助你解决掉它们。


快速入门

dynamic 可以这么用:

dynamic foo = GetSomeInstance();
foo.Run("欢迎访问吕毅(lvyi)的博客:blog.walterlv.com");

object GetSomeInstance()
{
    return 诡异的东西;
}

我们的 GetSomeInstance 明明返回的是 object,我们却可以调用真实类中的方法。

接下来讲述使用 dynamic 过程中可能会遇到的问题和解决方法。

编译错误:缺少编译器要求的成员

你初次在你的项目中引入 dynamic 关键字后,会出现编译错误,提示 “缺少编译器要求的成员”。

error CS0656: 缺少编译器要求的成员“Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create”

对于 .NET Core 或者 .NET Standard 项目

需要为你的项目安装以下两个 NuGet 包:

引用两个 NuGet 包

于是你的项目里面会多出两个引用:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <TargetFrameworks>netstandard2.0;net48</TargetFrameworks>
      </PropertyGroup>

      <ItemGroup>
++      <PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
++      <PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" />
      </ItemGroup>

    </Project>

对于 .NET Framework 项目

你需要引用 Microsoft.CSharp

添加引用

引用 Microsoft.CSharp

于是你的项目里面会多出一项引用:

    <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

      <PropertyGroup>
        <TargetFramework>net48</TargetFramework>
      </PropertyGroup>

      <ItemGroup>
++      <Reference Include="Microsoft.CSharp" />
      </ItemGroup>

    </Project>

异常:“{0}”未包含“{1}”的定义

{0} 是类型名称,而 {1} 是使用 dynamic 访问的属性或者方法的名称。

比如,我试图从某个 Attribute 中访问到 Key 属性的时候会抛出以下异常:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:““System.Attribute”未包含“Key”的定义”

出现此异常的原因是:

  • dynamic 所引用的对象里面,没有签名相同的 public 的属性或者方法

于是,如果你确认你的类型里面是有这个属性或者方法的话,那么就需要注意需要将此成员改成 public 才可以访问。


参考资料

设计一个 .NET 可用的弱引用集合(可用来做缓存池使用)

dotnet 职业技术学院 发布于 2019-08-04

我们有弱引用 WeakReference<T> 可以用来保存可被垃圾回收的对象,也有可以保存键值对的 ConditionalWeakTable

我们经常会考虑制作缓存池。虽然一般不推荐这么设计,但是你可以使用本文所述的方法和代码作为按垃圾回收缓存的缓存池的设计。


设计思路

既然现有 WeakReference<T>ConditionalWeakTable 可以帮助我们实现弱引用,那么我们可以考虑封装这两个类中的任何一个或者两个来帮助我们完成弱引用集合。

ConditionalWeakTable 类型仅仅在 internal 级别可以访问到集合中的所有的元素,对外开放的接口当中是无法拿到集合中的所有元素的,仅仅能根据 Key 来找到 Value 而已。所以如果要根据 ConditionalWeakTable 来实现弱引用集合那么需要自己记录集合中的所有的 Key,而这样的话我们依然需要自己实现一个用来记录所有 Key 的弱引用集合,相当于鸡生蛋蛋生鸡的问题。

所以我们考虑直接使用 WeakReference<T> 来实现弱引用集合。

自己维护一个列表 List<WeakReference<T>>,对外开放的 API 只能访问到其中未被垃圾回收到的对象。

设计原则

在设计此类型的时候,有一个非常大的需要考虑的因素,就是此类型中的元素个数是不确定的,如果设计不当,那么此类型的使用者可能写出的每一行代码都是 Bug。

你可以参考我的另一篇博客了解设计这种不确定类型的 API 的时候的一些指导:

总结起来就是:

  • 必须提供一个单一的方法,能够完成一些典型场景下某一时刻确定性状态的获取
  • 绝不能提供一些可能多次调用获取状态的方法

那么这个原则怎么体现在此弱引用集合的类型设计上呢?

设计实践

分析踩坑

IList<T>

我们来看看 IList<T> 接口是否可行:

public class WeakCollection<T> : IList<T> where T : class
{
    public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
    public int Count => throw new NotImplementedException();
    public bool IsReadOnly => throw new NotImplementedException();
    public void Add(T item) => throw new NotImplementedException();
    public void Clear() => throw new NotImplementedException();
    public bool Contains(T item) => throw new NotImplementedException();
    public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
    public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
    public int IndexOf(T item) => throw new NotImplementedException();
    public void Insert(int index, T item) => throw new NotImplementedException();
    public bool Remove(T item) => throw new NotImplementedException();
    public void RemoveAt(int index) => throw new NotImplementedException();
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

this[]CountIsReadOnlyContainsCopyToIndexOfGetEnumerator 这些都是在获取状态,AddClearRemove 是在修改状态,而 InsertRemoveAt 会在修改状态的同时读取状态。

这么多的获取和修改状态的方法,如果提供出去,还指望使用者能够正常使用,简直是做梦!违背以上两个原则。

ICollection<T>

那我们看看 IList<T> 的底层集合 ICollection<T>,实际上并没有解决问题,所以依然排除不能用!

    public class WeakCollection<T> : ICollection<T> where T : class
    {
--      public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
        public int Count => throw new NotImplementedException();
        public bool IsReadOnly => throw new NotImplementedException();
        public void Add(T item) => throw new NotImplementedException();
        public void Clear() => throw new NotImplementedException();
        public bool Contains(T item) => throw new NotImplementedException();
        public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
        public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
--      public int IndexOf(T item) => throw new NotImplementedException();
--      public void Insert(int index, T item) => throw new NotImplementedException();
        public bool Remove(T item) => throw new NotImplementedException();
--      public void RemoveAt(int index) => throw new NotImplementedException();
        IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
    }

不过,AddRemove 方法可能我们会考虑留下来,但这就不能是继承自 ICollection<T> 了。

IEnumerable<T>

IEnumerable<T> 里面只有两个方法,看起来少多了,那么我们能用吗?

public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();

这个方法仅供 foreach 使用,本来如果只是如此的话,问题还不是很大,但针对 IEnumerator<T> 有一大堆的 Linq 扩展方法,于是这相当于给此弱引用集合提供了大量可以用来读取状态的方法。

这依然非常危险!

使用者随时可能使用其中一个扩展方法得到了其中一个状态,随后使用另一个扩展方法得知其第二个状态,例如:

// 判断集合中是否存在 IFoo 类型以及是否存在 IBar 类型。
var hasFoo = weakList.OfType<IFoo>().Any();
var hasBar = weakList.OfType<IBar>().Any();

对具有并发开发经验的你来说,以上方法第一眼就能识别出这是不正确的写法。然而类型既然已经开放出去给大家使用了,那么这就非常危险。关键是这不是一个并发场景,于是开发者可能更难感受到在同一个上下文中调用两个方法将得到不确定的结果。对于并发可以使用锁,但对于弱引用,没有可以使用的相关方法来快速解决问题。

因此,IEnumerable<T> 也是不能继承的。

object

看来,我们只能继承自单纯的 object 基类了。此类型没有对托管来说可见的状态,于是谁也不会多次读取状态造成状态不确定了。

因此,我们需要自行实现所有场景下的 API。

动手

弱引用集合我们需要这些使用场景:

  • 向弱引用集合中添加一个元素 此场景下仅仅修改集合而不需要读取任何状态。
  • 向弱引用集合中移除一个元素 既然可以在参数中传入元素,说明此元素一定没有会垃圾回收;因此只要集合中还存在此元素,一定可以确定地移除,不会出现不确定的状态。
  • 在弱引用集合中找到符合要求的一个或多个元素 一旦满足要求,必须得到完全确定的结果,且在此结果保存的过程中一直生效。

可选考虑下面这些场景:

  • 清除所有元素 通常是为了复用某个缓存池的实例。

一定不能实现下面这些方法:

  • 判断是否存在某个元素 因为判断是否存在通常不是单独的操作,通常会使用此集合继续下一个操作,因此一定不能直接提供。
  • 其他在本文前面已经喷过不能添加进来的方法

于是,我们的 API 设计将是这样的:

public class WeakCollection<T> where T : class
{
    public void Add(T item) => throw new NotImplementedException();
    public bool Remove(T item) => throw new NotImplementedException();
    public void Clear() => throw new NotImplementedException();
    public T[] TryGetItems(Func<T, bool> filter) => throw new NotImplementedException();
}

完整代码

此类型已经以源代码包的形式发布到了 NuGet 上,你可以安装以下 NuGet 包阅读和使用其源代码:

安装后,你可以在你的项目中使用其源代码,并且可以直接使用 Ctrl + 鼠标点击的方式打开类型的源代码,而不需要进行反编译。

如何为非常不确定的行为(如并发)设计安全的 API,使用这些 API 时如何确保安全

dotnet 职业技术学院 发布于 2019-08-03

.NET 中提供了一些线程安全的类型,如 ConcurrentDictionary<TKey, TValue>,它们的 API 设计与常规设计差异很大。如果你对此觉得奇怪,那么正好阅读本文。本文介绍为这些非常不确定的行为设计 API 时应该考虑的原则,了解这些原则之后你会体会到为什么会有这些 API 设计上的差异,然后指导你设计新的类型。


不确定性

像并发集合一样,如 ConcurrentDictionary<TKey, TValue>ConcurrentQueue<T>,其设计为线程安全,于是它的每一个对外公开的方法调用都不会导致其内部状态错误。但是,你在调用其任何一个方法的时候,虽然调用的方法本身能够保证其线程安全,能够保证此方法涉及到的状态是确定的,但是一旦完成此方法的调用,其状态都将再次不确定。你只能依靠其方法的返回值来使用刚刚调用那一刻确定的状态。

我们来看几段代码:

var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
if (isRunning is 1)
{
    // 当前已经在执行队列,因此无需继续执行。
}
private ConcurrentDictionary<string, object> KeyValues { get; }
    = new ConcurrentDictionary<string, object>();

object Get(string key)
{
    var value = KeyValues.TryGetValue(key, out var v) ? v : null;
    return value;
}

这两段代码都使用到了可能涉及线程安全的一些代码。前者使用 Interlocked 做原则操作,而后者使用并发字典。

无论写上面哪一段代码,都面临着问题:

  • 此刻调用的那一句话得到的任何结果都仅仅只表示这一刻,而不代表其他任何代码时的结果。

比如前者的 Interlocked.CompareExchange(ref _isRunning, 1, 0) 我们得到一个返回值 isRunning,然后判断这个返回值。但是我们绝对不能够判断 _isRunning 这个字段,因为这个字段非常易变,在你的任何一个代码上下文中都可能变成你不希望看到的值。Interlocked 是原子操作,所以才确保安全。

而后者,此时访问得到的字典数据,和下一时刻访问得到的字典数据将可能完全不匹配,两次的数据不能通用。

API 用法指导

如果你正在为一个易变的状态设计 API,或者说你需要编写的类型带有很强的不确定性(类型状态的变化可能发生在任何一行代码上),那么你需要遵循一些设计原则才能确保安全。

同一个上下文仅能查看或修改一次状态

比如要为缓存设计一个获取可用实例的方法,可以使用:

private ConcurrentDictionary<string, object> KeyValues { get; }
    = new ConcurrentDictionary<string, object>();

void Get(string key)
{
    // CreateCachedInstance 是一个工厂方法,所有 GetOrAdd 的地方都是用此工厂方法创建。
    var value = KeyValues.GetOrAdd(key, CreateCachedInstance);
    return value;
}

但是绝对不能使用:

if(!KeyValues.TryGetValue(key, out var v))
{
    KeyValues.TryAdd(key, CreateCachedInstance(key));
}

这一段代码就是对并发的状态 KeyValues 做了两次访问。

ConcurrentDictionary 也正是考虑到了这种设计场景,于是才提供了 API GetOrAdd 方法。让你在获取对象实例的时候可以通过工厂方法去创建实例。

如果你需要设计这种状态极易变的 API,那么需要针对一些典型的设计场景提供一次调用就能获取此时此刻所有状态的方法。就像上文的 GetOrAdd 一样。

另一个例子,WeakReference<T> 弱引用对象的管理也是在一个方法里面可以获取到一个绝对确定的状态,而避免使用方进行两次判断:

if (weak.TryGetTarget(out var value))
{
    // 一旦这里拿到了对象,这个对象一定会存在且可用。
}

一定不能提供两个方法调用来完成这样的事情(比如先判断是否存在再获取对象的实例,就像 .NET Framework 4.0 和早期版本弱引用的 API 设计一样)。

对于并发,如果有多次查看或者修改状态,必须加锁

比如以下方法,是试图一个接一个地依次执行 _queue 中的所有任务。

虽然我们使用 Interlocked.CompareExchange 原子操作,但因为后面依然涉及到了多次状态的获取,导致不得不加锁才能确保安全。我们依然使用原则操作是为了避免单纯 lock 带来的性能损耗。

private volatile int _isRunning;
private readonly object _locker = new object();
private readonly ConcurrentQueue<TaskWrapper> _queue = new ConcurrentQueue<TaskWrapper>();

private async void Run()
{
    var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
    if (isRunning is 1)
    {
        lock (_locker)
        {
            if (_isRunning is 1)
            {
                // 当前已经在执行队列,因此无需继续执行。
                return;
            }
        }
    }

    var hasTask = true;
    while (hasTask)
    {
        // 当前还没有任何队列开始执行,因此需要开始执行队列。
        while (_queue.TryDequeue(out var wrapper))
        {
            // 内部已包含异常处理,因此外面可以无需捕获或者清理。
            await wrapper.RunAsync().ConfigureAwait(false);
        }

        lock (_locker)
        {
            hasTask = _queue.TryPeek(out _);
            if (!hasTask)
            {
                _isRunning = 0;
            }
        }
    }
}

这段代码的完全解读:

  1. 当执行 Run 方法的时候,先判断当前是否已经在跑其他的任务:
    • isRunning0 表示当前一定没有在跑其他任务,我们使用原则操作立刻将其修改为 1
    • isRunning1 表示当前不确定是否在跑其他任务;
  2. 既然 isRunning1 的时候状态不确定,于是我们加锁来判断其是否真的有任务在跑:
    • lock 环境中确认 _isRunning 字段而非变量为 1 则说明真的有任务在跑,此时等待任务完成即可,这里就可以退出了;
    • lock 环境中发现 _isRunning 字段而非变量为 0 则说明实际上是没有任务在跑的(刚刚判断为 1 只是因为这两次判断之间,并发的任务刚刚在结束的过程中),于是需要跟一开始判断为 0 一样,进入到后面的循环中;
  3. 外层的 while 循环第一次是一定能进去的,于是我们暂且不谈;
  4. while 内循环中,我们依次检查并发队列 _queue 中是否还有任务要执行,如果有要执行的,就执行:
    • 这个过程我们完全没有做加锁,因为这可能是非常耗时的任务,如果我们加锁,将导致其他线程出现非常严重的资源浪费;
  5. 如果 queue 中的所有任务执行完毕,我们将进入一个 lock 区间:
    • 在这个 lock 区间里面我们再次确认任务是否已经完成,如果没有完成,我们靠最外层的 while 循环重新回到内层 while 循环中继续任务;
    • 如果在这个 lock 区间里面我们发现任务已经完成了,就设置 _isRunning0,表示任务真的已经完成,随后退出 while 循环;

你可以注意到我们的 lock 是用来确认一开始 isRunning1 时的那个不确定的状态的。因为我们需要多次访问这个状态,所以必须加锁来确认状态是同步的。

API 设计指导

在了解了上面的用法指导后,API 设计指导也呼之欲出了:

  1. 针对典型的应用场景,必须设计一个专门的方法,一次调用即可完全获取当时需要的状态,或者一次调用即可完全修改需要修改的状态;
  2. 不要提供大于 1 个方法组合在一起才能使用的 API,这会让调用方获取不一致的状态。

对于多线程并发导致的不确定性,使用方虽然可以通过 lock 来规避以上第二条问题,但设计方最好在设计之初就避免问题,以便让 API 更好使用。

关于通用 API 设计指导,你可以阅读我的另一篇双语博客:

通过 AppSwitch 禁用 WPF 内置的触摸让 WPF 程序可以处理 Windows 触摸消息

dotnet 职业技术学院 发布于 2019-08-01

WPF 框架自己实现了一套触摸机制,但同一窗口只能支持一套触摸机制,于是这会禁用系统的触摸消息(WM_TOUCH)。这能够很大程度提升 WPF 程序的触摸响应速度,但是很多时候又会产生一些 Bug。

如果你有需要,可以考虑禁用 WPF 的内置的实时触摸(RealTimeStylus)。本文介绍禁用方法,使用 AppSwitch,而不是网上广为流传的反射方法。


如何设置 AppSwitch

在你的应用程序的 app.config 文件中加入 Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true 开关,即可关闭 WPF 内置的实时触摸,而改用 Windows 触摸消息(WM_TOUCH)。

<configuration>
  <runtime>
    <AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true" />
  </runtime>
</configuration>

如果你的解决方案中没有找到 app.config 文件,可以创建一个:

新建文件

应用程序配置文件

然后,把上面的代码拷贝进去即可。

反射禁用的方法

微软的官方文档也有提到使用放射禁用的方法,但一般不推荐这种调用内部 API 的方式,比较容易在 .NET 的版本更新中出现问题:

此方法可以解决的问题一览

拖拽窗口或者调整窗口大小时不能实时跟随的问题

在部分设备上启动即崩溃

在透明窗口上触摸会挡住 UWP 程序


参考资料

2019-7-30-WPF文本(2)-当显示文本时我们到底在做什么(2)

dotnet 职业技术学院 发布于 2019-07-30

上一讲我们介绍了如何获取文本字符,这一讲介绍文本的布局


我们之前在2019-7-29-WPF文本(1)-当显示文本时我们到底在做什么(1) - huangtengxiao介绍过,文本渲染需要经历找字符、measure、arrange、render过程。这里我们统一介绍measure和arrange过程(Layout过程)

Layout

首先我们思考下一群文本字符应该怎么对齐呢?上对齐?下对齐?

并不是,这群文字采用了一种奇妙的对齐方式,按照红线的那种不上不下的对齐,Baseline对齐

1564477195262

什么是baseline呢,简单来说就是用于文本对齐的基准。毕竟不是所有的文字都和汉字一样写的方方正正的,还是有一些语言的布局为了整体的美观需要各个字符能够上下调节。所以基线能够控制字符的对齐。

有了基线还有什么呢?有的同学马上说到width和height。

有同学说这个这么简单,需要讲么?需要。

我们看看下面的图片。f的选择宽度要远小于字符的呈现宽度,而f的呈现大小“入侵”了其他字符的区域。因此简单的width和height肯定不能满足这些需求。

1564646625433

1564646641226

我们看下软件中字符的各个尺寸表示,以WPF为例。下图的black box指的是字符的显示图像的大小,或者说这个字符的Geometry.Bounds。中间的横线就是字符的Baseline,用于字符的对齐。而Advance Width则是我们选择时看到的宽度,或者说是整个字符在文本中布局使用的宽度。而字符图形在渲染时的相对位置则是由left side bearing, Right side bearing, Upright baseline offset共同确定的。

1564647203976

由此,我们可以确定:

  • 字符的布局宽度(advance width)=左侧偏移(left side bearing)+右侧偏移(Right side bearing)+字符图像宽度
  • 字符的布局高度(advance height)=字符图像高度+竖直偏移(Upright baseline offset)
  • 字符布局位置由baseline origin确定

那么这么多的数据应该放在哪里呢?当然是放在字体文件的元数据中咯,这样就可以大幅减少软件层面的计算量。

OK,花了大力气介绍单个字符的布局,那么多行,多段落就会相对简单。他们和我们普通元素布局类似,只要指定位置将字符一个个”码”上去就行了。

参考链接:

Visual Studio 2019 中使用 .NET Core 预览版 SDK 的全局配置文件在哪里?

dotnet 职业技术学院 发布于 2019-07-29

本文介绍在使用 Visual Studio 2019 或者命令行执行 MSBuild dotnet build 命令时,决定是否使用 .NET Core SDK 预览版的全局配置文件。


指定是否使用 .NET Core 预览版 SDK 的全局配置文件在:

  • %LocalAppData%\Microsoft\VisualStudio\16.0_xxxxxxxx\sdk.txt

其中 %LocalAppData% 是 Windows 系统中自带的环境变量,16.0_xxxxxxxx 在不同的 Visual Studio 版本下不同。

比如,我的路径就是 C:\Users\lvyi\AppData\Local\Microsoft\VisualStudio\16.0_0b1a4ea6\sdk.txt

这个文件的内容非常简单,只有一行:

UsePreviews=True

sdk.txt 的所在路径

你一定觉得奇怪,我们在 Visual Studio 2019 中设置了使用 .NET Core SDK 预览版之后,这个配置是全局生效的,即便在命令行中运行 MSBuild 或者 dotnet build 也是会因此而使用预览版或者正式版的。但是这个路径明显看起来是 Visual Studio 的私有路径。

虽然这很诡异,但确实如此,不信,可以看我是如何确认这个文件就是 .NET Core SDK 预览版的全局配置的:

另外,如果你想知道如何在 Visual Studio 2019 中指定使用 .NET Core SDK 的预览版,可以参考我的另外一篇博客:

Visual Studio 2019 的

2019-7-29-WPF文本(1)-当显示文本时我们到底在做什么(1)

dotnet 职业技术学院 发布于 2019-07-29

文本显示是任何软件最重要的功能之一。但是很少有同学去关注文本的底层运作原理。这个系列将会介绍什么是文本的一些逻辑,以及如何利用我们的WPF现有接口,对文本进行最大程度定制化。


首先我们要明确,对于软件布局渲染来说,无论是文本,图片还是其他控件,其根本操作都是在指定区域显示指定大小的特定图像。由此我们可以推出,软件要完成某个特定对象布局渲染,需要1、测量对象的指定大小以及可以用于布局渲染的空间大小;2、确认对象放置的位置;3、将对象信息交给渲染线程进行绘制。对于WPF来说,有一套特定的模板方法去处理该流程:Measure、Arrange、Render。而对于文本来说,在此之前还需要有一个过程——找到对应字体的字符。

找字符

我们知道对于特定的文字,只要其编码方式相同,对应的编码数值就是确定的。但是虽然文本的编码相同,但是其显示图像却可以多种多样。以“黄腾霄好瘦”为例,下图分别是宋体、楷体、华文行楷、隶书字体下的显示状态。我们可以看到,同样的文字,显示样式各不相同,因为其对应的字体不同。

1564369493419

什么是字体(FontFamily)

字体可以理解为一个图形字典,它提供了指定“字符编码”到“字符图形”的映射关系。在windows的字符映射表中,我们可以看到这种映射关系。所以我们就能够通过字体和字符编码找到对应文本的形状。

1564370263324

那么加粗和斜体是怎么做的呢?我们看下宋体情况下”黄腾霄好瘦”这些文字的常规、加粗和斜体的显示状态。这里为了大家能够看清楚,特地调大了字号。

1564370869353

看出些情况了么?这里揭晓答案。首先是加粗。加粗很简单,你可以理解就是在常规字体上使用同样前景色的笔迹进行了一次描边,于是它就变粗了。

1564371280980

那么斜体呢?我们加两个背景框看看。看出来了吧,实际上一个就是一个skewtransform的事情嘛。

1564371903990

真的这么简单么?当然不是,我们来看一个反例,下图是Minion Pro的常规和斜体呈现的字符。最明显的是其中的f字母,根本就是两个不同的图好吧,怎么可能使用简单的transform变过来。

1564383640711

OK,那怎么办呢?easy,再制造一套字符,专门表示斜体时的图案。

我们在字体预览和相关设置中,可以找到Minion Pro字体,可以看到,其常规和斜体实际对应着不同的字体文件

1564384088039

1564384219815

那么粗体加斜体怎么办?造!粗体有不同的粗细程度,bold,semibold,medium?造!字体还有FontStretch控制字体横向伸展……造!造!造!

所以你可以看到Minion Pro有10个字体文件组成。当然如果你愿意,可以针对每一种字体粗细(FontWeight),斜体(FontStyle)都造出一个字体。如果你不愿意,当然也可以通过一种字体,让系统通过描边和SkewTransform的方式造出其他字体。

所以FontFamily,FontWeight,FontStyle,FontStretch可以确定一个字符的形状

1564384395424

听着很美妙了是不是,那么有没有想过如果这个编码的字符没有在字体中找到怎么办?

都没有还玩个锤子呀,显示个框框!

如果这么做,一定会被骂死掉。我们期望是用户的输入最大可能正确呈现。而且每个字体开发者尽可能少地开发重复的字体。

以Microsoft Himalaya为例,这款字体是针对藏文显示的字体。但是里面没有包含中文、日文、俄文等语言的字符。那么如果在这种字体下显示中文字符会怎么样呢?

1564385949866

除了一点点向上的偏移,其余都显示正常。

1564386357816

这是为什么呢?实际上,当我们在一种字体中没有找到对应的字符时,我们会采用FallBack 方案,在包含个字符编码的字体中选择一个,作为备用字体进行渲染。所以如果你当前字体没有包含该编码字符,没有关系,只要你的系统中,有其他字体能够显示就OK。但是如果所有字体都无法显示的话,那就GG了

参考链接:

Typography in WPF - Microsoft Docs

The partial same C# namespace may cause source code compatibility issue

dotnet 职业技术学院 发布于 2019-07-27

You might just add some simple APIs in your library and you’ll not think that will break down your compatibility. But actually, it might, that is – the source-code compatibility.


This post is written in multiple languages. Please select yours:

Assume that we’ve written a project P which references another two libraries A and B. And we have a Walterlv.A.Diagnostics.Foo class in library A.

using Walterlv.A;
using Walterlv.B;

namespace Walterlv.Demo
{
    class Hello
    {
        Run(Diagnostics.Foo foo)
        {
        }
    }
}

And now we add a new class Walterlv.B.Diagnostics.Bar class into the B library. That is adding a new API only.

Unfortunately, the code above would fail to compile because of the ambiguity of Diagnostics namespace. The Foo class cannot be found in an ambiguity namespace.

I write this post down to tell you that there may be source-code compatibility issue even if you only upgrade your library by simply adding APIs.